Skip to content

Commit 33d341e

Browse files
authored
feat(bundles): retrieve attestation from bundles (#1781)
Signed-off-by: Jose I. Paris <[email protected]>
1 parent 846a06e commit 33d341e

22 files changed

+778
-626
lines changed

app/controlplane/api/controlplane/v1/response_messages.pb.go

Lines changed: 379 additions & 365 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/controlplane/v1/response_messages.proto

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,11 @@ enum RunStatus {
8787

8888
message AttestationItem {
8989
// encoded DSEE envelope
90-
bytes envelope = 3;
91-
// sha256sum of the envelope in json format, used as a key in the CAS backend
90+
bytes envelope = 3 [deprecated=true];
91+
// Attestation bundle
92+
bytes bundle = 10;
93+
// sha256sum of the bundle containing the envelope, or the envelope in old attestations
94+
// used as a key in the CAS backend
9295
string digest_in_cas_backend = 7;
9396

9497
// denormalized envelope/statement content

app/controlplane/api/gen/frontend/controlplane/v1/response_messages.ts

Lines changed: 26 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.jsonschema.json

Lines changed: 7 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationItem.schema.json

Lines changed: 7 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/controlplane/internal/dispatcher/dispatcher.go

Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,19 +118,16 @@ func (d *FanOutDispatcher) Run(ctx context.Context, opts *RunOpts) error {
118118
Team: wf.Team,
119119
},
120120
WorkflowRun: &sdk.ChainloopMetadataWorkflowRun{
121-
ID: opts.WorkflowRunID,
122-
State: wfRun.State,
123-
StartedAt: *wfRun.CreatedAt,
124-
FinishedAt: *wfRun.FinishedAt,
125-
RunnerType: wfRun.RunnerType,
126-
RunURL: wfRun.RunURL,
121+
ID: opts.WorkflowRunID,
122+
State: wfRun.State,
123+
StartedAt: *wfRun.CreatedAt,
124+
FinishedAt: *wfRun.FinishedAt,
125+
RunnerType: wfRun.RunnerType,
126+
RunURL: wfRun.RunURL,
127+
AttestationDigest: wfRun.Attestation.Digest,
127128
},
128129
}
129130

130-
if wfRun.Attestation != nil {
131-
workflowMetadata.WorkflowRun.AttestationDigest = wfRun.Attestation.Digest
132-
}
133-
134131
// Dispatch the integrations
135132
for _, item := range queue {
136133
req := generateRequest(item, workflowMetadata)

app/controlplane/internal/service/attestation.go

Lines changed: 53 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ package service
1717

1818
import (
1919
"context"
20-
"encoding/base64"
2120
"encoding/json"
2221
"fmt"
2322
"sort"
@@ -30,13 +29,11 @@ import (
3029
"github.com/chainloop-dev/chainloop/app/controlplane/internal/usercontext/attjwtmiddleware"
3130
"github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz"
3231
casJWT "github.com/chainloop-dev/chainloop/internal/robotaccount/cas"
32+
"github.com/chainloop-dev/chainloop/pkg/attestation"
3333
"github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop"
3434
"github.com/chainloop-dev/chainloop/pkg/credentials"
35-
protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1"
36-
"google.golang.org/protobuf/encoding/protojson"
37-
3835
errors "github.com/go-kratos/kratos/v2/errors"
39-
"github.com/secure-systems-lab/go-securesystemslib/dsse"
36+
v1 "github.com/google/go-containerregistry/pkg/v1"
4037
)
4138

4239
type AttestationService struct {
@@ -189,90 +186,79 @@ func (s *AttestationService) Init(ctx context.Context, req *cpAPI.AttestationSer
189186
}
190187

191188
func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationServiceStoreRequest) (*cpAPI.AttestationServiceStoreResponse, error) {
192-
var envelope dsse.Envelope
193189
robotAccount := usercontext.CurrentRobotAccount(ctx)
194190
if robotAccount == nil {
195191
return nil, errors.NotFound("not found", "robot account not found")
196192
}
197193

198-
// Try unmarshalling a bundle first, falling back to plain dsse envelopes
199-
var bundle protobundle.Bundle
200-
// nolint: gocritic
201-
if req.GetBundle() != nil {
202-
if err := protojson.Unmarshal(req.GetBundle(), &bundle); err != nil {
203-
return nil, handleUseCaseErr(err, s.log)
204-
}
205-
envelope = *envelopeFromBundle(&bundle)
206-
} else if req.GetAttestation() != nil {
207-
// trying with envelope instead
208-
if err := json.Unmarshal(req.GetAttestation(), &envelope); err != nil {
209-
return nil, handleUseCaseErr(err, s.log)
210-
}
211-
} else {
212-
return nil, errors.BadRequest("attestation", "DSSE envelope or attestation bundle is required")
194+
if req.GetAttestation() == nil && req.GetBundle() == nil {
195+
return nil, errors.BadRequest("input required", "DSSE envelope or attestation bundle is required")
213196
}
214197

215-
digest, err := s.storeAttestation(ctx, &envelope, &bundle, robotAccount, req.WorkflowRunId, req.MarkVersionAsReleased)
198+
// This will make sure the provided workflowRunID belongs to the org encoded in the robot account
199+
wf, err := s.findWorkflowFromTokenOrNameOrRunID(ctx, robotAccount.OrgID, "", "", req.WorkflowRunId)
216200
if err != nil {
217201
return nil, handleUseCaseErr(err, s.log)
218202
}
219203

220-
return &cpAPI.AttestationServiceStoreResponse{
221-
Result: &cpAPI.AttestationServiceStoreResponse_Result{Digest: digest},
222-
}, nil
223-
}
204+
wRun, err := s.wrUseCase.GetByIDInOrgOrPublic(ctx, robotAccount.OrgID, req.WorkflowRunId)
205+
if err != nil {
206+
return nil, handleUseCaseErr(err, s.log)
207+
} else if wRun == nil {
208+
return nil, errors.NotFound("not found", "workflow run not found")
209+
}
224210

225-
// Extracts a DSSE envelope from a Sigstore bundle (Sigstore bundles have their own protobuf implementation for DSSE)
226-
func envelopeFromBundle(bundle *protobundle.Bundle) *dsse.Envelope {
227-
sigstoreEnvelope := bundle.GetDsseEnvelope()
228-
return &dsse.Envelope{
229-
PayloadType: sigstoreEnvelope.PayloadType,
230-
Payload: base64.StdEncoding.EncodeToString(sigstoreEnvelope.Payload),
231-
Signatures: []dsse.Signature{
232-
{
233-
KeyID: sigstoreEnvelope.GetSignatures()[0].GetKeyid(),
234-
Sig: string(sigstoreEnvelope.GetSignatures()[0].GetSig()),
235-
},
236-
},
211+
if len(wRun.CASBackends) == 0 {
212+
return nil, errors.NotFound("not found", "workflow run has no CAS backend")
237213
}
238-
}
239214

240-
// Stores and process a DSSE Envelope with a Chainloop attestation
241-
func (s *AttestationService) storeAttestation(ctx context.Context, envelope *dsse.Envelope, bundle *protobundle.Bundle, robotAccount *usercontext.RobotAccount, workflowRunID string, markAsReleased *bool) (string, error) {
242-
// This will make sure the provided workflowRunID belongs to the org encoded in the robot account
243-
wf, err := s.findWorkflowFromTokenOrNameOrRunID(ctx, robotAccount.OrgID, "", "", workflowRunID)
215+
digest, err := s.storeAttestation(ctx, req.GetAttestation(), req.GetBundle(), robotAccount, wf, wRun, req.MarkVersionAsReleased)
244216
if err != nil {
245-
return "", handleUseCaseErr(err, s.log)
217+
return nil, handleUseCaseErr(err, s.log)
246218
}
247219

248-
wRun, err := s.wrUseCase.GetByIDInOrgOrPublic(ctx, robotAccount.OrgID, workflowRunID)
220+
return &cpAPI.AttestationServiceStoreResponse{
221+
Result: &cpAPI.AttestationServiceStoreResponse_Result{Digest: digest.String()},
222+
}, nil
223+
}
224+
225+
// Stores and process a DSSE Envelope with a Chainloop attestation
226+
func (s *AttestationService) storeAttestation(ctx context.Context, envelope []byte, bundle []byte, robotAccount *usercontext.RobotAccount, wf *biz.Workflow, wfRun *biz.WorkflowRun, markAsReleased *bool) (*v1.Hash, error) {
227+
workflowRunID := wfRun.ID.String()
228+
casBackend := wfRun.CASBackends[0]
229+
230+
// extract structured envelope for integrations
231+
dsseEnv, err := attestation.DSSEEnvelopeFromRaw(bundle, envelope)
249232
if err != nil {
250-
return "", handleUseCaseErr(err, s.log)
251-
} else if wRun == nil {
252-
return "", errors.NotFound("not found", "workflow run not found")
233+
return nil, handleUseCaseErr(err, s.log)
253234
}
254235

255-
if len(wRun.CASBackends) == 0 {
256-
return "", errors.NotFound("not found", "workflow run has no CAS backend")
236+
// Store the attestation
237+
digest, err := s.wrUseCase.SaveAttestation(ctx, workflowRunID, envelope, bundle)
238+
if err != nil {
239+
return nil, handleUseCaseErr(err, s.log)
257240
}
258241

259-
// We currently only support one backend per workflowRun
260-
casBackend := wRun.CASBackends[0]
261-
262242
// If we have an external CAS backend, we will push there the attestation
263243
if !casBackend.Inline {
264244
go func() {
265245
b := backoff.NewExponentialBackOff()
266246
b.MaxElapsedTime = 1 * time.Minute
267247
err := backoff.Retry(
268248
func() error {
249+
rawContent := bundle
250+
if rawContent == nil {
251+
rawContent = envelope
252+
}
253+
269254
// reset context
270255
ctx := context.Background()
271-
digest, err := s.attestationUseCase.UploadToCAS(ctx, envelope, casBackend, workflowRunID)
272-
if err != nil {
256+
var err error
257+
if err = s.attestationUseCase.UploadAttestationToCAS(ctx, rawContent, casBackend, workflowRunID, *digest); err != nil {
273258
return err
274259
}
275-
s.log.Infow("msg", "attestation uploaded to CAS", "digest", digest.String(), "runID", workflowRunID)
260+
261+
s.log.Infow("msg", "attestation uploaded to CAS", "digest", digest, "runID", workflowRunID)
276262
return nil
277263
}, b)
278264

@@ -282,28 +268,22 @@ func (s *AttestationService) storeAttestation(ctx context.Context, envelope *dss
282268
}()
283269
}
284270

285-
// Store the attestation including the digest in the CAS backend (if exists)
286-
digest, err := s.wrUseCase.SaveAttestation(ctx, workflowRunID, envelope, bundle)
287-
if err != nil {
288-
return "", handleUseCaseErr(err, s.log)
289-
}
290-
291271
// Store the exploded attestation referrer information in the DB
292-
if err := s.referrerUseCase.ExtractAndPersist(ctx, envelope, wf.ID.String()); err != nil {
293-
return "", handleUseCaseErr(err, s.log)
272+
if err := s.referrerUseCase.ExtractAndPersist(ctx, dsseEnv, *digest, wf.ID.String()); err != nil {
273+
return nil, handleUseCaseErr(err, s.log)
294274
}
295275

296276
if !casBackend.Inline {
297277
// Store the mappings in the DB
298-
references, err := s.casMappingUseCase.LookupDigestsInAttestation(envelope)
278+
references, err := s.casMappingUseCase.LookupDigestsInAttestation(dsseEnv, *digest)
299279
if err != nil {
300-
return "", handleUseCaseErr(err, s.log)
280+
return nil, handleUseCaseErr(err, s.log)
301281
}
302282

303283
for _, ref := range references {
304284
s.log.Infow("msg", "creating CAS mapping", "name", ref.Name, "digest", ref.Digest, "workflowRun", workflowRunID, "casBackend", casBackend.ID.String())
305285
if _, err := s.casMappingUseCase.Create(ctx, ref.Digest, casBackend.ID.String(), workflowRunID); err != nil {
306-
return "", handleUseCaseErr(err, s.log)
286+
return nil, handleUseCaseErr(err, s.log)
307287
}
308288
}
309289
}
@@ -313,7 +293,7 @@ func (s *AttestationService) storeAttestation(ctx context.Context, envelope *dss
313293
// Run integrations dispatcher
314294
go func() {
315295
if err := s.integrationDispatcher.Run(context.TODO(), &dispatcher.RunOpts{
316-
Envelope: envelope, OrgID: robotAccount.OrgID, WorkflowID: wf.ID.String(),
296+
Envelope: dsseEnv, OrgID: robotAccount.OrgID, WorkflowID: workflowRunID,
317297
DownloadBackendType: string(casBackend.Provider),
318298
DownloadSecretName: secretName,
319299
WorkflowRunID: workflowRunID,
@@ -325,17 +305,17 @@ func (s *AttestationService) storeAttestation(ctx context.Context, envelope *dss
325305
// promote release if the workflowRun is successful
326306
if markAsReleased != nil && *markAsReleased {
327307
// Update the project version to mark it as a release
328-
if _, err := s.projectVersionUseCase.UpdateReleaseStatus(ctx, wRun.ProjectVersion.ID.String(), true); err != nil {
329-
return "", handleUseCaseErr(err, s.log)
308+
if _, err := s.projectVersionUseCase.UpdateReleaseStatus(ctx, wfRun.ProjectVersion.ID.String(), true); err != nil {
309+
return nil, handleUseCaseErr(err, s.log)
330310
}
331311
}
332312

333313
if err := s.wrUseCase.MarkAsFinished(ctx, workflowRunID, biz.WorkflowRunSuccess, ""); err != nil {
334-
return "", handleUseCaseErr(err, s.log)
314+
return nil, handleUseCaseErr(err, s.log)
335315
}
336316

337317
// Record the attestation in the prometheus registry
338-
_ = s.prometheusUseCase.ObserveAttestationIfNeeded(ctx, wRun, biz.WorkflowRunSuccess)
318+
_ = s.prometheusUseCase.ObserveAttestationIfNeeded(ctx, wfRun, biz.WorkflowRunSuccess)
339319

340320
return digest, nil
341321
}
@@ -487,6 +467,7 @@ func bizAttestationToPb(att *biz.Attestation) (*cpAPI.AttestationItem, error) {
487467
Blocked: policyEvaluationStatus.Blocked,
488468
HasViolations: policyEvaluationStatus.HasViolations,
489469
},
470+
Bundle: att.Bundle,
490471
}, nil
491472
}
492473

0 commit comments

Comments
 (0)