diff --git a/app/cli/pkg/action/attestation_push.go b/app/cli/pkg/action/attestation_push.go index cb6845596..07aab5d66 100644 --- a/app/cli/pkg/action/attestation_push.go +++ b/app/cli/pkg/action/attestation_push.go @@ -263,7 +263,7 @@ func (action *AttestationPush) Run(ctx context.Context, attestationID string, ru workflow := crafter.CraftingState.Attestation.GetWorkflow() - attestationResult.Digest, err = pushToControlPlane(ctx, action.CPConnection, envelope, bundle, workflow.GetWorkflowRunId(), workflow.GetVersion().GetMarkAsReleased()) + attestationResult.Digest, err = pushToControlPlane(ctx, action.CPConnection, bundle, workflow.GetWorkflowRunId(), workflow.GetVersion().GetMarkAsReleased()) if err != nil { return nil, fmt.Errorf("pushing to control plane: %w", err) } @@ -303,32 +303,17 @@ func (action *AttestationPush) saveBundle(bundle *protobundle.Bundle) error { return nil } -func pushToControlPlane(ctx context.Context, conn *grpc.ClientConn, envelope *dsse.Envelope, bundle *protobundle.Bundle, workflowRunID string, markVersionAsReleased bool) (string, error) { - encodedBundle, err := encodeBundle(bundle) - if err != nil { - return "", fmt.Errorf("encoding attestation: %w", err) - } - - client := pb.NewAttestationServiceClient(conn) - - // if endpoint doesn't accept the bundle, we still send the plain attestation for backwards compatibility - encodedAttestation, err := encodeEnvelope(envelope) - if err != nil { - return "", fmt.Errorf("encoding attestation: %w", err) - } - +func pushToControlPlane(ctx context.Context, conn *grpc.ClientConn, bundle *protobundle.Bundle, workflowRunID string, markVersionAsReleased bool) (string, error) { // remove additional base64 encoding in signature. See https://github.com/chainloop-dev/chainloop/issues/1832 attestation.FixSignatureInBundle(bundle) - encodedFixedBundle, err := encodeBundle(bundle) + encodedBundle, err := encodeBundle(bundle) if err != nil { return "", fmt.Errorf("encoding attestation: %w", err) } - // Store bundle next versions will perform this in a single call) + client := pb.NewAttestationServiceClient(conn) resp, err := client.Store(ctx, &pb.AttestationServiceStoreRequest{ - Attestation: encodedAttestation, - Bundle: encodedBundle, - AttestationBundle: encodedFixedBundle, + AttestationBundle: encodedBundle, WorkflowRunId: workflowRunID, MarkVersionAsReleased: &markVersionAsReleased, }) @@ -339,10 +324,6 @@ func pushToControlPlane(ctx context.Context, conn *grpc.ClientConn, envelope *ds return resp.Result.Digest, nil } -func encodeEnvelope(e *dsse.Envelope) ([]byte, error) { - return json.Marshal(e) -} - func encodeBundle(b *protobundle.Bundle) ([]byte, error) { return protojson.Marshal(b) } diff --git a/app/controlplane/api/controlplane/v1/workflow_run.pb.go b/app/controlplane/api/controlplane/v1/workflow_run.pb.go index bc1a9f373..86e76a8fa 100644 --- a/app/controlplane/api/controlplane/v1/workflow_run.pb.go +++ b/app/controlplane/api/controlplane/v1/workflow_run.pb.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -739,14 +739,6 @@ func (x *AttestationServiceInitResponse) GetResult() *AttestationServiceInitResp type AttestationServiceStoreRequest struct { state protoimpl.MessageState `protogen:"open.v1"` - // encoded DSEE envelope - // - // Deprecated: Marked as deprecated in controlplane/v1/workflow_run.proto. - Attestation []byte `protobuf:"bytes,1,opt,name=attestation,proto3" json:"attestation,omitempty"` - // deprecated because of https://github.com/chainloop-dev/chainloop/issues/1832 - // - // Deprecated: Marked as deprecated in controlplane/v1/workflow_run.proto. - Bundle []byte `protobuf:"bytes,4,opt,name=bundle,proto3" json:"bundle,omitempty"` // encoded Sigstore attestation bundle AttestationBundle []byte `protobuf:"bytes,5,opt,name=attestation_bundle,json=attestationBundle,proto3" json:"attestation_bundle,omitempty"` WorkflowRunId string `protobuf:"bytes,2,opt,name=workflow_run_id,json=workflowRunId,proto3" json:"workflow_run_id,omitempty"` @@ -786,22 +778,6 @@ func (*AttestationServiceStoreRequest) Descriptor() ([]byte, []int) { return file_controlplane_v1_workflow_run_proto_rawDescGZIP(), []int{11} } -// Deprecated: Marked as deprecated in controlplane/v1/workflow_run.proto. -func (x *AttestationServiceStoreRequest) GetAttestation() []byte { - if x != nil { - return x.Attestation - } - return nil -} - -// Deprecated: Marked as deprecated in controlplane/v1/workflow_run.proto. -func (x *AttestationServiceStoreRequest) GetBundle() []byte { - if x != nil { - return x.Bundle - } - return nil -} - func (x *AttestationServiceStoreRequest) GetAttestationBundle() []byte { if x != nil { return x.AttestationBundle @@ -1817,14 +1793,12 @@ const file_controlplane_v1_workflow_run_proto_rawDesc = "" + "\x0eSigningOptions\x126\n" + "\x17timestamp_authority_url\x18\x01 \x01(\tR\x15timestampAuthorityUrl\x12\x1d\n" + "\n" + - "signing_ca\x18\x02 \x01(\tR\tsigningCa\"\x9d\x02\n" + - "\x1eAttestationServiceStoreRequest\x12$\n" + - "\vattestation\x18\x01 \x01(\fB\x02\x18\x01R\vattestation\x12\x1a\n" + - "\x06bundle\x18\x04 \x01(\fB\x02\x18\x01R\x06bundle\x12-\n" + + "signing_ca\x18\x02 \x01(\tR\tsigningCa\"\xfc\x01\n" + + "\x1eAttestationServiceStoreRequest\x12-\n" + "\x12attestation_bundle\x18\x05 \x01(\fR\x11attestationBundle\x12/\n" + "\x0fworkflow_run_id\x18\x02 \x01(\tB\a\xbaH\x04r\x02\x10\x01R\rworkflowRunId\x12<\n" + "\x18mark_version_as_released\x18\x03 \x01(\bH\x00R\x15markVersionAsReleased\x88\x01\x01B\x1b\n" + - "\x19_mark_version_as_released\"\x94\x01\n" + + "\x19_mark_version_as_releasedJ\x04\b\x01\x10\x02J\x04\b\x04\x10\x05R\vattestationR\x06bundle\"\x94\x01\n" + "\x1fAttestationServiceStoreResponse\x12O\n" + "\x06result\x18\x01 \x01(\v27.controlplane.v1.AttestationServiceStoreResponse.ResultR\x06result\x1a \n" + "\x06Result\x12\x16\n" + diff --git a/app/controlplane/api/controlplane/v1/workflow_run.proto b/app/controlplane/api/controlplane/v1/workflow_run.proto index 76aba91aa..48cabc315 100644 --- a/app/controlplane/api/controlplane/v1/workflow_run.proto +++ b/app/controlplane/api/controlplane/v1/workflow_run.proto @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -157,16 +157,15 @@ message AttestationServiceInitResponse { } message AttestationServiceStoreRequest { - // encoded DSEE envelope - bytes attestation = 1 [deprecated = true]; - // deprecated because of https://github.com/chainloop-dev/chainloop/issues/1832 - bytes bundle = 4 [deprecated = true]; // encoded Sigstore attestation bundle bytes attestation_bundle = 5; string workflow_run_id = 2 [(buf.validate.field).string = {min_len: 1}]; // mark the associated version as released optional bool mark_version_as_released = 3; + + reserved 1, 4; + reserved "attestation", "bundle"; } message AttestationServiceStoreResponse { diff --git a/app/controlplane/api/controlplane/v1/workflow_run_grpc.pb.go b/app/controlplane/api/controlplane/v1/workflow_run_grpc.pb.go index e3618133f..216637d69 100644 --- a/app/controlplane/api/controlplane/v1/workflow_run_grpc.pb.go +++ b/app/controlplane/api/controlplane/v1/workflow_run_grpc.pb.go @@ -1,5 +1,5 @@ // -// Copyright 2024-2025 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/app/controlplane/api/gen/frontend/controlplane/v1/workflow_run.ts b/app/controlplane/api/gen/frontend/controlplane/v1/workflow_run.ts index e93832f03..eaa841007 100644 --- a/app/controlplane/api/gen/frontend/controlplane/v1/workflow_run.ts +++ b/app/controlplane/api/gen/frontend/controlplane/v1/workflow_run.ts @@ -134,18 +134,6 @@ export interface AttestationServiceInitResponse_SigningOptions { } export interface AttestationServiceStoreRequest { - /** - * encoded DSEE envelope - * - * @deprecated - */ - attestation: Uint8Array; - /** - * deprecated because of https://github.com/chainloop-dev/chainloop/issues/1832 - * - * @deprecated - */ - bundle: Uint8Array; /** encoded Sigstore attestation bundle */ attestationBundle: Uint8Array; workflowRunId: string; @@ -1537,23 +1525,11 @@ export const AttestationServiceInitResponse_SigningOptions = { }; function createBaseAttestationServiceStoreRequest(): AttestationServiceStoreRequest { - return { - attestation: new Uint8Array(0), - bundle: new Uint8Array(0), - attestationBundle: new Uint8Array(0), - workflowRunId: "", - markVersionAsReleased: undefined, - }; + return { attestationBundle: new Uint8Array(0), workflowRunId: "", markVersionAsReleased: undefined }; } export const AttestationServiceStoreRequest = { encode(message: AttestationServiceStoreRequest, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - if (message.attestation.length !== 0) { - writer.uint32(10).bytes(message.attestation); - } - if (message.bundle.length !== 0) { - writer.uint32(34).bytes(message.bundle); - } if (message.attestationBundle.length !== 0) { writer.uint32(42).bytes(message.attestationBundle); } @@ -1573,20 +1549,6 @@ export const AttestationServiceStoreRequest = { while (reader.pos < end) { const tag = reader.uint32(); switch (tag >>> 3) { - case 1: - if (tag !== 10) { - break; - } - - message.attestation = reader.bytes(); - continue; - case 4: - if (tag !== 34) { - break; - } - - message.bundle = reader.bytes(); - continue; case 5: if (tag !== 42) { break; @@ -1619,8 +1581,6 @@ export const AttestationServiceStoreRequest = { fromJSON(object: any): AttestationServiceStoreRequest { return { - attestation: isSet(object.attestation) ? bytesFromBase64(object.attestation) : new Uint8Array(0), - bundle: isSet(object.bundle) ? bytesFromBase64(object.bundle) : new Uint8Array(0), attestationBundle: isSet(object.attestationBundle) ? bytesFromBase64(object.attestationBundle) : new Uint8Array(0), @@ -1631,10 +1591,6 @@ export const AttestationServiceStoreRequest = { toJSON(message: AttestationServiceStoreRequest): unknown { const obj: any = {}; - message.attestation !== undefined && - (obj.attestation = base64FromBytes(message.attestation !== undefined ? message.attestation : new Uint8Array(0))); - message.bundle !== undefined && - (obj.bundle = base64FromBytes(message.bundle !== undefined ? message.bundle : new Uint8Array(0))); message.attestationBundle !== undefined && (obj.attestationBundle = base64FromBytes( message.attestationBundle !== undefined ? message.attestationBundle : new Uint8Array(0), @@ -1652,8 +1608,6 @@ export const AttestationServiceStoreRequest = { object: I, ): AttestationServiceStoreRequest { const message = createBaseAttestationServiceStoreRequest(); - message.attestation = object.attestation ?? new Uint8Array(0); - message.bundle = object.bundle ?? new Uint8Array(0); message.attestationBundle = object.attestationBundle ?? new Uint8Array(0); message.workflowRunId = object.workflowRunId ?? ""; message.markVersionAsReleased = object.markVersionAsReleased ?? undefined; diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceStoreRequest.jsonschema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceStoreRequest.jsonschema.json index 737e89d06..1a026e49f 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceStoreRequest.jsonschema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceStoreRequest.jsonschema.json @@ -18,21 +18,11 @@ } }, "properties": { - "attestation": { - "description": "encoded DSEE envelope", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", - "type": "string" - }, "attestationBundle": { "description": "encoded Sigstore attestation bundle", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "type": "string" }, - "bundle": { - "description": "deprecated because of https://github.com/chainloop-dev/chainloop/issues/1832", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", - "type": "string" - }, "markVersionAsReleased": { "description": "mark the associated version as released", "type": "boolean" diff --git a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceStoreRequest.schema.json b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceStoreRequest.schema.json index 44574d5da..12abf0571 100644 --- a/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceStoreRequest.schema.json +++ b/app/controlplane/api/gen/jsonschema/controlplane.v1.AttestationServiceStoreRequest.schema.json @@ -18,21 +18,11 @@ } }, "properties": { - "attestation": { - "description": "encoded DSEE envelope", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", - "type": "string" - }, "attestation_bundle": { "description": "encoded Sigstore attestation bundle", "pattern": "^[A-Za-z0-9+/]*={0,2}$", "type": "string" }, - "bundle": { - "description": "deprecated because of https://github.com/chainloop-dev/chainloop/issues/1832", - "pattern": "^[A-Za-z0-9+/]*={0,2}$", - "type": "string" - }, "mark_version_as_released": { "description": "mark the associated version as released", "type": "boolean" diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index 35e49ec00..9feeca93c 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -245,11 +245,7 @@ func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationSe bundle := req.GetAttestationBundle() if bundle == nil { - bundle = req.GetBundle() - } - - if req.GetAttestation() == nil && bundle == nil { - return nil, errors.BadRequest("input required", "DSSE envelope or attestation bundle is required") + return nil, errors.BadRequest("input required", "attestation bundle is required") } // This will make sure the provided workflowRunID belongs to the org encoded in the robot account @@ -274,7 +270,7 @@ func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationSe return nil, errors.NotFound("not found", "workflow run has no CAS backend") } - digest, err := s.storeAttestation(ctx, req.GetAttestation(), bundle, robotAccount, wf, wRun, req.MarkVersionAsReleased) + digest, err := s.storeAttestation(ctx, bundle, robotAccount, wf, wRun, req.MarkVersionAsReleased) if err != nil { return nil, handleUseCaseErr(err, s.log) } @@ -284,19 +280,19 @@ func (s *AttestationService) Store(ctx context.Context, req *cpAPI.AttestationSe }, nil } -// Stores and process a DSSE Envelope with a Chainloop attestation -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) { +// storeAttestation stores and processes a Sigstore attestation bundle. +func (s *AttestationService) storeAttestation(ctx context.Context, bundle []byte, robotAccount *usercontext.RobotAccount, wf *biz.Workflow, wfRun *biz.WorkflowRun, markAsReleased *bool) (*v1.Hash, error) { workflowRunID := wfRun.ID.String() casBackend := wfRun.CASBackends[0] // extract structured envelope for integrations - dsseEnv, err := attestation.DSSEEnvelopeFromRaw(bundle, envelope) + dsseEnv, err := attestation.DSSEEnvelopeFromBundleBytes(bundle) if err != nil { return nil, handleUseCaseErr(err, s.log) } // Store the attestation - digest, err := s.wrUseCase.SaveAttestation(ctx, workflowRunID, envelope, bundle) + digest, err := s.wrUseCase.SaveAttestation(ctx, workflowRunID, bundle) if err != nil { return nil, handleUseCaseErr(err, s.log) } @@ -315,15 +311,9 @@ func (s *AttestationService) storeAttestation(ctx context.Context, envelope []by b.MaxElapsedTime = 1 * time.Minute err := backoff.Retry( func() error { - rawContent := bundle - if rawContent == nil { - rawContent = envelope - } - // reset context ctx := context.Background() - var err error - if err = s.attestationUseCase.UploadAttestationToCAS(ctx, rawContent, casBackend, workflowRunID, *digest); err != nil { + if err := s.attestationUseCase.UploadAttestationToCAS(ctx, bundle, casBackend, workflowRunID, *digest); err != nil { return err } diff --git a/app/controlplane/pkg/biz/mocks/WorkflowRunRepo.go b/app/controlplane/pkg/biz/mocks/WorkflowRunRepo.go index e74004195..7ca0b57da 100644 --- a/app/controlplane/pkg/biz/mocks/WorkflowRunRepo.go +++ b/app/controlplane/pkg/biz/mocks/WorkflowRunRepo.go @@ -11,7 +11,6 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/biz" "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" "github.com/google/uuid" - "github.com/secure-systems-lab/go-securesystemslib/dsse" mock "github.com/stretchr/testify/mock" ) @@ -674,38 +673,38 @@ func (_c *WorkflowRunRepo_MarkAsFinished_Call) RunAndReturn(run func(ctx context return _c } -// SaveAttestation provides a mock function for the type WorkflowRunRepo -func (_mock *WorkflowRunRepo) SaveAttestation(ctx context.Context, ID uuid.UUID, att *dsse.Envelope, digest string) error { - ret := _mock.Called(ctx, ID, att, digest) +// SaveAttestationBundle provides a mock function for the type WorkflowRunRepo +func (_mock *WorkflowRunRepo) SaveAttestationBundle(ctx context.Context, ID uuid.UUID, digest string, bundle []byte) error { + ret := _mock.Called(ctx, ID, digest, bundle) if len(ret) == 0 { - panic("no return value specified for SaveAttestation") + panic("no return value specified for SaveAttestationBundle") } var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, *dsse.Envelope, string) error); ok { - r0 = returnFunc(ctx, ID, att, digest) + if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, string, []byte) error); ok { + r0 = returnFunc(ctx, ID, digest, bundle) } else { r0 = ret.Error(0) } return r0 } -// WorkflowRunRepo_SaveAttestation_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveAttestation' -type WorkflowRunRepo_SaveAttestation_Call struct { +// WorkflowRunRepo_SaveAttestationBundle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveAttestationBundle' +type WorkflowRunRepo_SaveAttestationBundle_Call struct { *mock.Call } -// SaveAttestation is a helper method to define mock.On call +// SaveAttestationBundle is a helper method to define mock.On call // - ctx context.Context // - ID uuid.UUID -// - att *dsse.Envelope // - digest string -func (_e *WorkflowRunRepo_Expecter) SaveAttestation(ctx interface{}, ID interface{}, att interface{}, digest interface{}) *WorkflowRunRepo_SaveAttestation_Call { - return &WorkflowRunRepo_SaveAttestation_Call{Call: _e.mock.On("SaveAttestation", ctx, ID, att, digest)} +// - bundle []byte +func (_e *WorkflowRunRepo_Expecter) SaveAttestationBundle(ctx interface{}, ID interface{}, digest interface{}, bundle interface{}) *WorkflowRunRepo_SaveAttestationBundle_Call { + return &WorkflowRunRepo_SaveAttestationBundle_Call{Call: _e.mock.On("SaveAttestationBundle", ctx, ID, digest, bundle)} } -func (_c *WorkflowRunRepo_SaveAttestation_Call) Run(run func(ctx context.Context, ID uuid.UUID, att *dsse.Envelope, digest string)) *WorkflowRunRepo_SaveAttestation_Call { +func (_c *WorkflowRunRepo_SaveAttestationBundle_Call) Run(run func(ctx context.Context, ID uuid.UUID, digest string, bundle []byte)) *WorkflowRunRepo_SaveAttestationBundle_Call { _c.Call.Run(func(args mock.Arguments) { var arg0 context.Context if args[0] != nil { @@ -715,13 +714,13 @@ func (_c *WorkflowRunRepo_SaveAttestation_Call) Run(run func(ctx context.Context if args[1] != nil { arg1 = args[1].(uuid.UUID) } - var arg2 *dsse.Envelope + var arg2 string if args[2] != nil { - arg2 = args[2].(*dsse.Envelope) + arg2 = args[2].(string) } - var arg3 string + var arg3 []byte if args[3] != nil { - arg3 = args[3].(string) + arg3 = args[3].([]byte) } run( arg0, @@ -733,75 +732,12 @@ func (_c *WorkflowRunRepo_SaveAttestation_Call) Run(run func(ctx context.Context return _c } -func (_c *WorkflowRunRepo_SaveAttestation_Call) Return(err error) *WorkflowRunRepo_SaveAttestation_Call { - _c.Call.Return(err) - return _c -} - -func (_c *WorkflowRunRepo_SaveAttestation_Call) RunAndReturn(run func(ctx context.Context, ID uuid.UUID, att *dsse.Envelope, digest string) error) *WorkflowRunRepo_SaveAttestation_Call { - _c.Call.Return(run) - return _c -} - -// SaveBundle provides a mock function for the type WorkflowRunRepo -func (_mock *WorkflowRunRepo) SaveBundle(ctx context.Context, ID uuid.UUID, bundle []byte) error { - ret := _mock.Called(ctx, ID, bundle) - - if len(ret) == 0 { - panic("no return value specified for SaveBundle") - } - - var r0 error - if returnFunc, ok := ret.Get(0).(func(context.Context, uuid.UUID, []byte) error); ok { - r0 = returnFunc(ctx, ID, bundle) - } else { - r0 = ret.Error(0) - } - return r0 -} - -// WorkflowRunRepo_SaveBundle_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SaveBundle' -type WorkflowRunRepo_SaveBundle_Call struct { - *mock.Call -} - -// SaveBundle is a helper method to define mock.On call -// - ctx context.Context -// - ID uuid.UUID -// - bundle []byte -func (_e *WorkflowRunRepo_Expecter) SaveBundle(ctx interface{}, ID interface{}, bundle interface{}) *WorkflowRunRepo_SaveBundle_Call { - return &WorkflowRunRepo_SaveBundle_Call{Call: _e.mock.On("SaveBundle", ctx, ID, bundle)} -} - -func (_c *WorkflowRunRepo_SaveBundle_Call) Run(run func(ctx context.Context, ID uuid.UUID, bundle []byte)) *WorkflowRunRepo_SaveBundle_Call { - _c.Call.Run(func(args mock.Arguments) { - var arg0 context.Context - if args[0] != nil { - arg0 = args[0].(context.Context) - } - var arg1 uuid.UUID - if args[1] != nil { - arg1 = args[1].(uuid.UUID) - } - var arg2 []byte - if args[2] != nil { - arg2 = args[2].([]byte) - } - run( - arg0, - arg1, - arg2, - ) - }) - return _c -} - -func (_c *WorkflowRunRepo_SaveBundle_Call) Return(err error) *WorkflowRunRepo_SaveBundle_Call { +func (_c *WorkflowRunRepo_SaveAttestationBundle_Call) Return(err error) *WorkflowRunRepo_SaveAttestationBundle_Call { _c.Call.Return(err) return _c } -func (_c *WorkflowRunRepo_SaveBundle_Call) RunAndReturn(run func(ctx context.Context, ID uuid.UUID, bundle []byte) error) *WorkflowRunRepo_SaveBundle_Call { +func (_c *WorkflowRunRepo_SaveAttestationBundle_Call) RunAndReturn(run func(ctx context.Context, ID uuid.UUID, digest string, bundle []byte) error) *WorkflowRunRepo_SaveAttestationBundle_Call { _c.Call.Return(run) return _c } diff --git a/app/controlplane/pkg/biz/workflowrun.go b/app/controlplane/pkg/biz/workflowrun.go index 5947a3578..8eab3c953 100644 --- a/app/controlplane/pkg/biz/workflowrun.go +++ b/app/controlplane/pkg/biz/workflowrun.go @@ -93,8 +93,7 @@ type WorkflowRunRepo interface { FindByAttestationDigest(ctx context.Context, digest string) (*WorkflowRun, error) FindByIDInOrg(ctx context.Context, orgID, ID uuid.UUID) (*WorkflowRun, error) MarkAsFinished(ctx context.Context, ID uuid.UUID, status WorkflowRunStatus, reason string) error - SaveAttestation(ctx context.Context, ID uuid.UUID, att *dsse.Envelope, digest string) error - SaveBundle(ctx context.Context, ID uuid.UUID, bundle []byte) error + SaveAttestationBundle(ctx context.Context, ID uuid.UUID, digest string, bundle []byte) error GetBundle(ctx context.Context, wrID uuid.UUID) ([]byte, error) UpdatePolicyViolationsStatus(ctx context.Context, ID uuid.UUID, hasPolicyViolations bool) error List(ctx context.Context, orgID uuid.UUID, f *RunListFilters, p *pagination.CursorOptions) ([]*WorkflowRun, string, error) @@ -326,25 +325,20 @@ func (uc *WorkflowRunUseCase) MarkAsFinished(ctx context.Context, id string, sta return uc.wfRunRepo.MarkAsFinished(ctx, runID, status, reason) } -func (uc *WorkflowRunUseCase) SaveAttestation(ctx context.Context, id string, envelope, bundle []byte) (*v1.Hash, error) { +func (uc *WorkflowRunUseCase) SaveAttestation(ctx context.Context, id string, bundle []byte) (*v1.Hash, error) { runID, err := uuid.Parse(id) if err != nil { return nil, NewErrInvalidUUID(err) } - rawContent := bundle - if rawContent == nil { - rawContent = envelope - } - // calculate the content digest // Todo: this should be calculated in the use case - digest, _, err := v1.SHA256(bytes.NewReader(rawContent)) + digest, _, err := v1.SHA256(bytes.NewReader(bundle)) if err != nil { return nil, fmt.Errorf("failed to calculate SHA256 of attestation: %w", err) } - dsseEnv, err := attestation.DSSEEnvelopeFromRaw(bundle, envelope) + dsseEnv, err := attestation.DSSEEnvelopeFromBundleBytes(bundle) if err != nil { return nil, fmt.Errorf("extracting DSSE envelope: %w", err) } @@ -356,7 +350,7 @@ func (uc *WorkflowRunUseCase) SaveAttestation(ctx context.Context, id string, en } // verify attestation (only if chainloop is the signer) - validation, err := uc.verifyBundle(ctx, rawContent) + validation, err := uc.verifyBundle(ctx, bundle) if err != nil { if !errors.Is(err, verifier.ErrInvalidBundle) { return nil, err @@ -384,18 +378,8 @@ func (uc *WorkflowRunUseCase) SaveAttestation(ctx context.Context, id string, en } } - // Save bundle if provided, as it might come as an empty struct - if bundle != nil { - // Save bundle - if err = uc.wfRunRepo.SaveBundle(ctx, runID, bundle); err != nil { - return nil, fmt.Errorf("saving bundle: %w", err) - } - // do not store the envelope if bundle is already stored - dsseEnv = nil - } - - if err := uc.wfRunRepo.SaveAttestation(ctx, runID, dsseEnv, digest.String()); err != nil { - return nil, fmt.Errorf("saving attestation: %w", err) + if err := uc.wfRunRepo.SaveAttestationBundle(ctx, runID, digest.String(), bundle); err != nil { + return nil, fmt.Errorf("saving attestation bundle: %w", err) } // Extract and save policy violations status from the predicate diff --git a/app/controlplane/pkg/biz/workflowrun_integration_test.go b/app/controlplane/pkg/biz/workflowrun_integration_test.go index e3880b47a..9900669c5 100644 --- a/app/controlplane/pkg/biz/workflowrun_integration_test.go +++ b/app/controlplane/pkg/biz/workflowrun_integration_test.go @@ -134,44 +134,23 @@ func (s *workflowRunIntegrationTestSuite) TestSaveAttestation() { assert := assert.New(s.T()) ctx := context.Background() - validEnvelope, envelopeBytes := testEnvelope(s.T(), "testdata/attestations/full.json") - h, _, err := v2.SHA256(bytes.NewReader(envelopeBytes)) + bundle, bundleBytes := testBundle(s.T(), "testdata/attestations/bundle.json") + bundleHash, _, err := v2.SHA256(bytes.NewReader(bundleBytes)) require.NoError(s.T(), err) - s.T().Run("non existing workflowRun", func(t *testing.T) { - _, err := s.WorkflowRun.SaveAttestation(ctx, uuid.NewString(), envelopeBytes, nil) + s.T().Run("non existing workflowRun", func(_ *testing.T) { + _, err := s.WorkflowRun.SaveAttestation(ctx, uuid.NewString(), bundleBytes) assert.Error(err) assert.True(biz.IsNotFound(err)) }) - s.T().Run("valid workflowRun with envelope", func(_ *testing.T) { - run, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{ - WorkflowID: s.workflowOrg1.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID, - }) - assert.NoError(err) - - d, err := s.WorkflowRun.SaveAttestation(ctx, run.ID.String(), envelopeBytes, nil) - assert.NoError(err) - assert.Equal(h, *d) - - // Retrieve attestation ref from storage and compare - r, err := s.WorkflowRun.GetByIDInOrgOrPublic(ctx, s.org.ID, run.ID.String()) - assert.NoError(err) - assert.Equal(h.String(), r.Attestation.Digest) - assert.Equal(&biz.Attestation{Envelope: validEnvelope, Digest: h.String()}, r.Attestation) - }) - - bundle, bundleBytes := testBundle(s.T(), "testdata/attestations/bundle.json") - bundleHash, _, err := v2.SHA256(bytes.NewReader(bundleBytes)) - require.NoError(s.T(), err) - s.T().Run("succeeded workflowRun with bundle", func(_ *testing.T) { run, err := s.WorkflowRun.Create(ctx, &biz.WorkflowRunCreateOpts{ WorkflowID: s.workflowOrg1.ID.String(), ContractRevision: s.contractVersion, CASBackendID: s.casBackend.ID, }) assert.NoError(err) - d, err := s.WorkflowRun.SaveAttestation(ctx, run.ID.String(), envelopeBytes, bundleBytes) + d, err := s.WorkflowRun.SaveAttestation(ctx, run.ID.String(), bundleBytes) assert.NoError(err) assert.Equal(bundleHash, *d) @@ -192,7 +171,7 @@ func (s *workflowRunIntegrationTestSuite) TestSaveAttestation() { }) assert.NoError(err) - d, err := s.WorkflowRun.SaveAttestation(ctx, run.ID.String(), envelopeBytes, bundleBytes) + d, err := s.WorkflowRun.SaveAttestation(ctx, run.ID.String(), bundleBytes) assert.NoError(err) assert.Equal(bundleHash, *d) exists, err := s.Data.DB.Attestation.Query().Where(attestation2.WorkflowrunID(run.ID)).Exist(ctx) @@ -489,6 +468,18 @@ func testBundle(t *testing.T, path string) (*v1.Bundle, []byte) { return &bundle, bundleJSON } +// testBundleBytesFromEnvelope wraps a DSSE envelope file into a Sigstore bundle and returns its protojson bytes. +func testBundleBytesFromEnvelope(t *testing.T, path string) []byte { + _, envBytes := testEnvelope(t, path) + var env dsse.Envelope + require.NoError(t, json.Unmarshal(envBytes, &env)) + b, err := attestation.BundleFromDSSEEnvelope(&env) + require.NoError(t, err) + out, err := protojson.Marshal(b) + require.NoError(t, err) + return out +} + const ( version1 = "v1" version2 = "v2" @@ -528,8 +519,8 @@ func setupWorkflowRunTestData(t *testing.T, suite *testhelpers.TestingUseCases, ProjectVersion: version1, }) assert.NoError(err) - _, envBytes := testEnvelope(t, "testdata/attestations/full.json") - d, err := suite.WorkflowRun.SaveAttestation(ctx, s.runOrg1.ID.String(), envBytes, nil) + bundleBytes := testBundleBytesFromEnvelope(t, "testdata/attestations/full.json") + d, err := suite.WorkflowRun.SaveAttestation(ctx, s.runOrg1.ID.String(), bundleBytes) assert.NoError(err) s.digestAtt1 = d.String() @@ -539,8 +530,8 @@ func setupWorkflowRunTestData(t *testing.T, suite *testhelpers.TestingUseCases, ProjectVersion: version1, }) assert.NoError(err) - _, envBytes = testEnvelope(t, "testdata/attestations/empty.json") - d, err = suite.WorkflowRun.SaveAttestation(ctx, s.runOrg2.ID.String(), envBytes, nil) + bundleBytes = testBundleBytesFromEnvelope(t, "testdata/attestations/empty.json") + d, err = suite.WorkflowRun.SaveAttestation(ctx, s.runOrg2.ID.String(), bundleBytes) assert.NoError(err) s.digestAttOrg2 = d.String() @@ -550,8 +541,8 @@ func setupWorkflowRunTestData(t *testing.T, suite *testhelpers.TestingUseCases, ProjectVersion: version2, }) assert.NoError(err) - _, envBytes = testEnvelope(t, "testdata/attestations/with-string.json") - d, err = suite.WorkflowRun.SaveAttestation(ctx, s.runOrg2Public.ID.String(), envBytes, nil) + bundleBytes = testBundleBytesFromEnvelope(t, "testdata/attestations/with-string.json") + d, err = suite.WorkflowRun.SaveAttestation(ctx, s.runOrg2Public.ID.String(), bundleBytes) assert.NoError(err) s.digestAttPublic = d.String() diff --git a/app/controlplane/pkg/data/workflowrun.go b/app/controlplane/pkg/data/workflowrun.go index d39f2b6e0..03f9de520 100644 --- a/app/controlplane/pkg/data/workflowrun.go +++ b/app/controlplane/pkg/data/workflowrun.go @@ -33,7 +33,6 @@ import ( "github.com/chainloop-dev/chainloop/app/controlplane/pkg/pagination" "github.com/go-kratos/kratos/v2/log" "github.com/google/uuid" - "github.com/secure-systems-lab/go-securesystemslib/dsse" ) type WorkflowRunRepo struct { @@ -204,37 +203,22 @@ func (r *WorkflowRunRepo) FindByIDInOrg(ctx context.Context, orgID, id uuid.UUID return entWrToBizWr(ctx, run) } -// SaveAttestation Saves the attestation for a workflow run in the database -func (r *WorkflowRunRepo) SaveAttestation(ctx context.Context, id uuid.UUID, att *dsse.Envelope, digest string) error { - q := r.data.DB.WorkflowRun.UpdateOneID(id). - SetAttestationDigest(digest) - - // the envelope will come empty in normal attestations, since bundles are stored separately - // But old CLIs might still send the envelope instead of the bundle. In those cases, we store it - // as before. But this is a DEPRECATED behaviour that will be removed eventually. - if att != nil { - // Set attestation when using old CLI versions - q.SetAttestation(att) - } - run, err := q.Save(ctx) - if err != nil && !ent.IsNotFound(err) { - return err - } else if run == nil { - return biz.NewErrNotFound(fmt.Sprintf("workflow run with id %s not found", id)) - } - - return nil -} - -// SaveBundle Save the bundle for a workflow run in the database -func (r *WorkflowRunRepo) SaveBundle(ctx context.Context, wrID uuid.UUID, bundle []byte) error { - if err := r.data.DB.Attestation.Create(). - SetBundle(bundle).SetWorkflowrunID(wrID). - Exec(ctx); err != nil { - return fmt.Errorf("saving bundle: %w", err) - } +// SaveAttestationBundle persists the attestation digest on the workflow run and the bundle bytes +// in the linked attestation row within a single transaction. +func (r *WorkflowRunRepo) SaveAttestationBundle(ctx context.Context, id uuid.UUID, digest string, bundle []byte) error { + return WithTx(ctx, r.data.DB, func(tx *ent.Tx) error { + if err := tx.WorkflowRun.UpdateOneID(id).SetAttestationDigest(digest).Exec(ctx); err != nil { + if ent.IsNotFound(err) { + return biz.NewErrNotFound(fmt.Sprintf("workflow run with id %s not found", id)) + } + return err + } - return nil + if err := tx.Attestation.Create().SetBundle(bundle).SetWorkflowrunID(id).Exec(ctx); err != nil { + return fmt.Errorf("saving bundle: %w", err) + } + return nil + }) } // UpdatePolicyViolationsStatus updates the policy violations status for a workflow run diff --git a/buf.yaml b/buf.yaml index 1375c638d..6ac0cff6d 100644 --- a/buf.yaml +++ b/buf.yaml @@ -52,6 +52,11 @@ modules: except: - EXTENSION_NO_DELETE - FIELD_SAME_DEFAULT + ignore_only: + # Deprecated `attestation` and `bundle` fields were removed in favor of `attestation_bundle`; + # their tag numbers and names are `reserved` in the proto to prevent accidental reuse. + FIELD_NO_DELETE: + - app/controlplane/api/controlplane/v1/workflow_run.proto - path: app/controlplane/internal/conf lint: use: diff --git a/pkg/attestation/attestations.go b/pkg/attestation/attestations.go index fb53ffc8f..2a03fd3ef 100644 --- a/pkg/attestation/attestations.go +++ b/pkg/attestation/attestations.go @@ -1,5 +1,5 @@ // -// Copyright 2024 The Chainloop Authors. +// Copyright 2024-2026 The Chainloop Authors. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -91,25 +91,20 @@ func BundleFromDSSEEnvelope(dsseEnvelope *dsse.Envelope) (*protobundle.Bundle, e }, nil } -func DSSEEnvelopeFromRaw(bundle, envelope []byte) (*dsse.Envelope, error) { - var dsseEnv dsse.Envelope - if bundle != nil { - var attBundle protobundle.Bundle - if err := protojson.Unmarshal(bundle, &attBundle); err != nil { - return nil, fmt.Errorf("unmarshalling bundle: %w", err) - } - dsseEnv = *DSSEEnvelopeFromBundle(&attBundle) - } else { - if err := json.Unmarshal(envelope, &dsseEnv); err != nil { - return nil, fmt.Errorf("unmarshalling envelope: %w", err) - } +// DSSEEnvelopeFromBundleBytes extracts a DSSE envelope from the protojson-encoded bytes of a Sigstore bundle. +// It validates that the bundle carries a DSSE envelope with at least one signature, since callers +// (and DSSEEnvelopeFromBundle) assume that invariant. +func DSSEEnvelopeFromBundleBytes(bundle []byte) (*dsse.Envelope, error) { + var attBundle protobundle.Bundle + if err := protojson.Unmarshal(bundle, &attBundle); err != nil { + return nil, fmt.Errorf("unmarshalling bundle: %w", err) } - return &dsseEnv, nil + if len(attBundle.GetDsseEnvelope().GetSignatures()) == 0 { + return nil, fmt.Errorf("invalid attestation bundle: missing DSSE signature") + } + return DSSEEnvelopeFromBundle(&attBundle), nil } -// TODO: remove this fix once `AttestationServiceStoreRequest.Bundle` is fully removed, -// and move the logic to BundleFromDSSEEnvelope method instead (where the bug is originated) - // FixSignatureInBundle removes any additional base64 encoding from the signature in the bundle. // Old attestations have signatures base64 encoded twice, see https://github.com/chainloop-dev/chainloop/issues/1832 func FixSignatureInBundle(bundle *protobundle.Bundle) {