Skip to content

Commit 8d85e75

Browse files
authored
feat(backend): ATTESTATION material type (#727)
Signed-off-by: Jose I. Paris <[email protected]>
1 parent f333385 commit 8d85e75

File tree

16 files changed

+367
-37
lines changed

16 files changed

+367
-37
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ Chainloop supports the collection of the following pieces of evidence types:
131131
- [SARIF](https://docs.oasis-open.org/sarif/sarif/v2.1.0/)
132132
- [JUnit](https://www.ibm.com/docs/en/developer-for-zos/14.1?topic=formats-junit-xml-format)
133133
- [Helm Chart](https://helm.sh/docs/topics/charts/)
134+
- Attestation: existing Chainloop attestations.
134135
- Artifact Type: It represents a software artifact.
135136
- Custom Evidence Type: Custom piece of evidence that doesn't fit in any other category, for instance, an approval report in json format, etc.
136137
- Key-Value metadata pairs

app/controlplane/api/gen/frontend/workflowcontract/v1/crafting_schema.ts

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

app/controlplane/api/workflowcontract/v1/crafting_schema.pb.go

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

app/controlplane/api/workflowcontract/v1/crafting_schema.proto

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,9 @@ message CraftingSchema {
8888
// Pieces of evidences represent generic, additional context that don't fit
8989
// into one of the well known material types. For example, a custom approval report (in json), ...
9090
EVIDENCE = 11;
91+
92+
// Chainloop attestation coming from a different workflow.
93+
ATTESTATION = 12;
9194
}
9295
}
9396
}

app/controlplane/internal/biz/attestation.go

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ package biz
1818
import (
1919
"bytes"
2020
"context"
21-
"encoding/json"
2221
"fmt"
2322
"io"
2423

24+
"github.com/chainloop-dev/chainloop/internal/attestation"
2525
"github.com/chainloop-dev/chainloop/internal/servicelogger"
2626
"github.com/go-kratos/kratos/v2/log"
2727

@@ -47,7 +47,7 @@ func NewAttestationUseCase(client CASClient, logger log.Logger) *AttestationUseC
4747

4848
func (uc *AttestationUseCase) UploadToCAS(ctx context.Context, envelope *dsse.Envelope, backend *CASBackend, workflowRunID string) (*cr_v1.Hash, error) {
4949
filename := fmt.Sprintf("attestation-%s.json", workflowRunID)
50-
jsonContent, h, err := jsonEnvelopeWithDigest(envelope)
50+
jsonContent, h, err := attestation.JSONEnvelopeWithDigest(envelope)
5151
if err != nil {
5252
return nil, fmt.Errorf("marshaling the envelope: %w", err)
5353
}
@@ -58,18 +58,3 @@ func (uc *AttestationUseCase) UploadToCAS(ctx context.Context, envelope *dsse.En
5858

5959
return &h, nil
6060
}
61-
62-
// jsonEnvelopeWithDigest returns the JSON content of the envelope and its digest.
63-
func jsonEnvelopeWithDigest(envelope *dsse.Envelope) ([]byte, cr_v1.Hash, error) {
64-
jsonContent, err := json.Marshal(envelope)
65-
if err != nil {
66-
return nil, cr_v1.Hash{}, fmt.Errorf("marshaling the envelope: %w", err)
67-
}
68-
69-
h, _, err := cr_v1.SHA256(bytes.NewBuffer(jsonContent))
70-
if err != nil {
71-
return nil, cr_v1.Hash{}, fmt.Errorf("calculating the digest: %w", err)
72-
}
73-
74-
return jsonContent, h, nil
75-
}

app/controlplane/internal/biz/referrer.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"time"
2525

2626
"github.com/chainloop-dev/chainloop/app/controlplane/internal/conf"
27+
"github.com/chainloop-dev/chainloop/internal/attestation"
2728
"github.com/chainloop-dev/chainloop/internal/attestation/renderer/chainloop"
2829
"github.com/chainloop-dev/chainloop/internal/servicelogger"
2930
"github.com/go-kratos/kratos/v2/log"
@@ -247,7 +248,7 @@ func (r *Referrer) MapID() string {
247248
// 4 - creating link between the attestation and the materials/subjects as needed
248249
// see tests for examples
249250
func extractReferrers(att *dsse.Envelope) ([]*Referrer, error) {
250-
_, h, err := jsonEnvelopeWithDigest(att)
251+
_, h, err := attestation.JSONEnvelopeWithDigest(att)
251252
if err != nil {
252253
return nil, fmt.Errorf("marshaling attestation: %w", err)
253254
}

app/controlplane/internal/biz/workflowrun.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"time"
2424

2525
"github.com/chainloop-dev/chainloop/app/controlplane/internal/pagination"
26+
"github.com/chainloop-dev/chainloop/internal/attestation"
2627
"github.com/secure-systems-lab/go-securesystemslib/dsse"
2728

2829
"github.com/go-kratos/kratos/v2/log"
@@ -248,7 +249,7 @@ func (uc *WorkflowRunUseCase) SaveAttestation(ctx context.Context, id string, en
248249
}
249250

250251
// Calculate the digest
251-
_, digest, err := jsonEnvelopeWithDigest(envelope)
252+
_, digest, err := attestation.JSONEnvelopeWithDigest(envelope)
252253
if err != nil {
253254
return "", NewErrValidation(fmt.Errorf("marshaling the envelope: %w", err))
254255
}

docs/docs/reference/operator/contract.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ Chainloop supports the collection of the following pieces of evidence types:
5656
- [SARIF](https://docs.oasis-open.org/sarif/sarif/v2.1.0/)
5757
- [JUnit](https://www.ibm.com/docs/en/developer-for-zos/14.1?topic=formats-junit-xml-format)
5858
- [Helm Chart](https://helm.sh/docs/topics/charts/)
59+
- Attestation: existing Chainloop attestations.
5960
- Artifact Type: It represents a software artifact.
6061
- Custom Evidence Type: Custom piece of evidence that doesn't fit in any other category, for instance, an approval report in json format, etc.
6162
- Key-Value metadata pairs
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// Copyright 2024 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 attestation
17+
18+
import (
19+
"bytes"
20+
"encoding/json"
21+
"fmt"
22+
23+
cr_v1 "github.com/google/go-containerregistry/pkg/v1"
24+
"github.com/secure-systems-lab/go-securesystemslib/dsse"
25+
)
26+
27+
// JSONEnvelopeWithDigest returns the JSON content of the envelope and its digest.
28+
func JSONEnvelopeWithDigest(envelope *dsse.Envelope) ([]byte, cr_v1.Hash, error) {
29+
jsonContent, err := json.Marshal(envelope)
30+
if err != nil {
31+
return nil, cr_v1.Hash{}, fmt.Errorf("marshaling the envelope: %w", err)
32+
}
33+
34+
h, _, err := cr_v1.SHA256(bytes.NewBuffer(jsonContent))
35+
if err != nil {
36+
return nil, cr_v1.Hash{}, fmt.Errorf("calculating the digest: %w", err)
37+
}
38+
39+
return jsonContent, h, nil
40+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
//
2+
// Copyright 2024 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 materials
17+
18+
import (
19+
"context"
20+
"encoding/json"
21+
"fmt"
22+
"os"
23+
"path"
24+
25+
schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
26+
"github.com/chainloop-dev/chainloop/internal/attestation"
27+
api "github.com/chainloop-dev/chainloop/internal/attestation/crafter/api/attestation/v1"
28+
"github.com/chainloop-dev/chainloop/internal/attestation/renderer/chainloop"
29+
"github.com/chainloop-dev/chainloop/internal/casclient"
30+
"github.com/rs/zerolog"
31+
"github.com/secure-systems-lab/go-securesystemslib/dsse"
32+
)
33+
34+
type AttestationCrafter struct {
35+
*crafterCommon
36+
backend *casclient.CASBackend
37+
}
38+
39+
// NewAttestationCrafter generates a new Attestation material.
40+
// Attestation materials represent a chainloop attestation submitted in a different workflow. This is useful to link
41+
// related workflow runs. For instance, the deployment of different microservices coming from a common build workflow.
42+
func NewAttestationCrafter(schema *schemaapi.CraftingSchema_Material, backend *casclient.CASBackend, l *zerolog.Logger) (*AttestationCrafter, error) {
43+
if schema.Type != schemaapi.CraftingSchema_Material_ATTESTATION {
44+
return nil, fmt.Errorf("material type is not attestation")
45+
}
46+
47+
craftCommon := &crafterCommon{logger: l, input: schema}
48+
return &AttestationCrafter{backend: backend, crafterCommon: craftCommon}, nil
49+
}
50+
51+
// Craft will calculate the digest of the artifact, simulate an upload and return the material definition
52+
func (i *AttestationCrafter) Craft(ctx context.Context, artifactPath string) (*api.Attestation_Material, error) {
53+
data, err := os.ReadFile(artifactPath)
54+
if err != nil {
55+
return nil, fmt.Errorf("artifact file cannot be read: %w", err)
56+
}
57+
var dsseEnvelope dsse.Envelope
58+
if err := json.Unmarshal(data, &dsseEnvelope); err != nil {
59+
return nil, fmt.Errorf("artifact is not a valid DSEE Envelope: %w", err)
60+
}
61+
62+
predicate, err := chainloop.ExtractPredicate(&dsseEnvelope)
63+
if err != nil {
64+
return nil, fmt.Errorf("the provided file does not seem to be a chainloop-generated attestation: %w", err)
65+
}
66+
67+
// regenerate the json from the parsed data, just to remove any formating from the incoming json and preserve
68+
// the digest
69+
jsonContent, _, err := attestation.JSONEnvelopeWithDigest(&dsseEnvelope)
70+
if err != nil {
71+
return nil, fmt.Errorf("creating CAS payload: %w", err)
72+
}
73+
74+
// Create a temp file with this content and upload to the CAS
75+
dir := os.TempDir()
76+
filename := fmt.Sprintf("%s-%s-attestation.json", predicate.GetMetadata().Name, predicate.GetMetadata().WorkflowRunID)
77+
78+
file, err := os.Create(path.Join(dir, filename))
79+
if err != nil {
80+
return nil, fmt.Errorf("failed to create temp file: %w", err)
81+
}
82+
defer file.Close()
83+
defer os.Remove(file.Name())
84+
85+
if _, err := file.Write(jsonContent); err != nil {
86+
return nil, fmt.Errorf("failed to write JSON payload: %w", err)
87+
}
88+
89+
return uploadAndCraft(ctx, i.input, i.backend, file.Name(), i.logger)
90+
}

0 commit comments

Comments
 (0)