diff --git a/app/controlplane/internal/service/attestation.go b/app/controlplane/internal/service/attestation.go index e4a9dd8f0..a46f5815f 100644 --- a/app/controlplane/internal/service/attestation.go +++ b/app/controlplane/internal/service/attestation.go @@ -26,7 +26,6 @@ import ( "time" "github.com/cenkalti/backoff/v4" - "github.com/in-toto/in-toto-golang/in_toto" cpAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" contractAPI "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" @@ -295,36 +294,13 @@ func (s *AttestationService) GetUploadCreds(ctx context.Context, _ *cpAPI.Attest return &cpAPI.AttestationServiceGetUploadCredsResponse{Result: &cpAPI.AttestationServiceGetUploadCredsResponse_Result{Token: t}}, nil } -func extractPredicate(envelope *dsse.Envelope) (*renderer.ChainloopProvenancePredicateV1, error) { - decodedPayload, err := envelope.DecodeB64Payload() - if err != nil { - return nil, err - } - - statement := &in_toto.Statement{} - if err := json.Unmarshal(decodedPayload, statement); err != nil { - return nil, fmt.Errorf("un-marshaling predicate: %w", err) - } - - var predicate *renderer.ChainloopProvenancePredicateV1 - if statement.PredicateType == renderer.ChainloopPredicateTypeV1 { - if predicate, err = extractPredicateV1(statement); err != nil { - return nil, fmt.Errorf("extracting predicate: %w", err) - } - } else { - return nil, errors.InternalServer("internal error", "predicate type not supported") - } - - return predicate, nil -} - func bizAttestationToPb(att *biz.Attestation) (*cpAPI.AttestationItem, error) { encodedAttestation, err := json.Marshal(att.Envelope) if err != nil { return nil, err } - predicate, err := extractPredicate(att.Envelope) + predicate, err := renderer.ExtractPredicate(att.Envelope) if err != nil { return nil, err } @@ -358,20 +334,6 @@ func extractMaterials(in []*renderer.ChainloopProvenanceMaterial) []*cpAPI.Attes return res } -func extractPredicateV1(statement *in_toto.Statement) (*renderer.ChainloopProvenancePredicateV1, error) { - jsonPredicate, err := json.Marshal(statement.Predicate) - if err != nil { - return nil, fmt.Errorf("un-marshaling predicate: %w", err) - } - - predicate := &renderer.ChainloopProvenancePredicateV1{} - if err := json.Unmarshal(jsonPredicate, predicate); err != nil { - return nil, fmt.Errorf("un-marshaling predicate: %w", err) - } - - return predicate, nil -} - type uploadSBOMToDepTrackOpts struct { envelope *dsse.Envelope orgID, workflowID string @@ -410,7 +372,7 @@ func uploadSBOMsToDependencyTrack(opts *uploadSBOMToDepTrackOpts) error { return nil } - predicate, err := extractPredicate(opts.envelope) + predicate, err := renderer.ExtractPredicate(opts.envelope) if err != nil { return err } diff --git a/internal/attestation/renderer/chainloop.go b/internal/attestation/renderer/chainloop.go index e8e31a853..17e41eb66 100644 --- a/internal/attestation/renderer/chainloop.go +++ b/internal/attestation/renderer/chainloop.go @@ -24,6 +24,7 @@ import ( v1 "github.com/chainloop-dev/chainloop/app/cli/api/attestation/v1" "github.com/in-toto/in-toto-golang/in_toto" + "github.com/secure-systems-lab/go-securesystemslib/dsse" schemaapi "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" slsacommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common" @@ -213,3 +214,44 @@ func outputChainloopMaterials(att *v1.Attestation, onlyOutput bool) []*Chainloop return res } + +// Extract the Chainloop attestation predicate from an encoded DSSE envelope +func ExtractPredicate(envelope *dsse.Envelope) (*ChainloopProvenancePredicateV1, error) { + decodedPayload, err := envelope.DecodeB64Payload() + if err != nil { + return nil, err + } + + // 1 - Extract the in-toto statement + statement := &in_toto.Statement{} + if err := json.Unmarshal(decodedPayload, statement); err != nil { + return nil, fmt.Errorf("un-marshaling predicate: %w", err) + } + + // 2 - Extract the Chainloop predicate from the in-toto statement + var predicate *ChainloopProvenancePredicateV1 + switch statement.PredicateType { + case ChainloopPredicateTypeV1: + if predicate, err = extractPredicateV1(statement); err != nil { + return nil, fmt.Errorf("extracting predicate: %w", err) + } + default: + return nil, fmt.Errorf("unsupported predicate type: %s", statement.PredicateType) + } + + return predicate, nil +} + +func extractPredicateV1(statement *in_toto.Statement) (*ChainloopProvenancePredicateV1, error) { + jsonPredicate, err := json.Marshal(statement.Predicate) + if err != nil { + return nil, fmt.Errorf("un-marshaling predicate: %w", err) + } + + predicate := &ChainloopProvenancePredicateV1{} + if err := json.Unmarshal(jsonPredicate, predicate); err != nil { + return nil, fmt.Errorf("un-marshaling predicate: %w", err) + } + + return predicate, nil +} diff --git a/internal/attestation/renderer/chainloop_test.go b/internal/attestation/renderer/chainloop_test.go new file mode 100644 index 000000000..40410b822 --- /dev/null +++ b/internal/attestation/renderer/chainloop_test.go @@ -0,0 +1,96 @@ +// +// Copyright 2023 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. +// 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 renderer + +import ( + "encoding/json" + "fmt" + "os" + "testing" + + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestExtractPredicate(t *testing.T) { + testCases := []struct { + name string + envelopePath string + predicatePath string + wantErr bool + }{ + { + name: "valid envelope", + envelopePath: "testdata/valid.envelope.json", + predicatePath: "testdata/valid.predicate.json", + wantErr: false, + }, + { + name: "unknown source attestation", + envelopePath: "testdata/unknown.envelope.json", + wantErr: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + envelope, err := testEnvelope(tc.envelopePath) + require.NoError(t, err) + + got, err := ExtractPredicate(envelope) + if tc.wantErr { + assert.Error(t, err) + return + } + + want, err := testPredicate(tc.predicatePath) + require.NoError(t, err) + + assert.NoError(t, err) + assert.Equal(t, want, got) + }) + } +} + +func testEnvelope(filePath string) (*dsse.Envelope, error) { + var envelope dsse.Envelope + content, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + err = json.Unmarshal(content, &envelope) + if err != nil { + return nil, err + } + + return &envelope, nil +} + +func testPredicate(path string) (*ChainloopProvenancePredicateV1, error) { + var predicate ChainloopProvenancePredicateV1 + content, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(content, &predicate); err != nil { + return nil, fmt.Errorf("un-marshaling predicate: %w", err) + } + + return &predicate, nil +} diff --git a/internal/attestation/renderer/testdata/unknown.envelope.json b/internal/attestation/renderer/testdata/unknown.envelope.json new file mode 100644 index 000000000..4b1c688f0 --- /dev/null +++ b/internal/attestation/renderer/testdata/unknown.envelope.json @@ -0,0 +1,10 @@ +{ + "payloadType": "application/vnd.in-toto+json", + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJ1bmtub3duLmNvbXBhbnkvYXR0ZXN0YXRpb24vdjAuMSIsInN1YmplY3QiOlt7Im5hbWUiOiJjaGFpbmxvb3AuZGV2L3dvcmtmbG93L2ZvbyIsImRpZ2VzdCI6eyJzaGEyNTYiOiIzMGE2ZTQ3NzAyMTQ4ZWE4NTUzYjNlOGZhYjA1OTUyYzMyOTdhNzMwM2EwZWY4MjA1ODAxM2U4NTg0NGFjYmQzIn19XSwicHJlZGljYXRlIjp7Im1ldGFkYXRhIjp7Im5hbWUiOiJmb28iLCJwcm9qZWN0IjoiYmFyIiwidGVhbSI6IiIsImluaXRpYWxpemVkQXQiOiIyMDIzLTAzLTIyVDEwOjE1OjQwLjczNTUzMTg0MVoiLCJmaW5pc2hlZEF0IjoiMjAyMy0wMy0yMlQxMToxNjoxOS45Njg0ODg4MzcrMDE6MDAiLCJ3b3JrZmxvd1J1bklEIjoiYTc3ZDhlNGUtNDE1My00ZWVlLTk4ZWQtN2E2MDg4OTNmMzFiIiwid29ya2Zsb3dJRCI6Ijk0YTgwOTcyLTA0YjAtNDIxNS1hZDk4LWE3NGQwODI2M2Y5ZiJ9LCJidWlsZGVyIjp7ImlkIjoiY2hhaW5sb29wLmRldi9jbGkvZGV2QHNoYTI1Njo2M2I2MDAxZjczY2Y3ZDI4ZGVjMzc3MjBlOGUwYjJmNzc5YTY5MDE4ZTgwYmRhY2JhNzY2ZDAxYjU5MDEwOThiIn0sImJ1aWxkVHlwZSI6ImNoYWlubG9vcC5kZXYvd29ya2Zsb3dydW4vdjAuMSIsInJ1bm5lclR5cGUiOiJSVU5ORVJfVFlQRV9VTlNQRUNJRklFRCJ9fQ==", + "signatures": [ + { + "keyid": "", + "sig": "MEQCICuT3+A0ub9e3VJ/wjPv+oPTnwUR1AT3IKlXyqUXcgphAiAOeX1hUNYM+rGl6NE1sbTwqag6unuuXceAZF3aNCJ9YQ==" + } + ] +} \ No newline at end of file diff --git a/internal/attestation/renderer/testdata/valid.envelope.json b/internal/attestation/renderer/testdata/valid.envelope.json new file mode 100644 index 000000000..af0b0edf2 --- /dev/null +++ b/internal/attestation/renderer/testdata/valid.envelope.json @@ -0,0 +1,10 @@ +{ + "payloadType": "application/vnd.in-toto+json", + "payload": "eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJjaGFpbmxvb3AuZGV2L2F0dGVzdGF0aW9uL3YwLjEiLCJzdWJqZWN0IjpbeyJuYW1lIjoiY2hhaW5sb29wLmRldi93b3JrZmxvdy9idWlsZC1hbmQtcmVsZWFzZSIsImRpZ2VzdCI6eyJzaGEyNTYiOiI0YzQ2YTYzOWU5ZDM5NWJhYjMyODk1NDk1YjNkODExODZmNzhlNTMxMGFjM2RhODU0Mzk0ZjgxOWExMDMxYzk3In19LHsibmFtZSI6ImdoY3IuaW8vY2hhaW5sb29wLWRldi9pbnRlZ3JhdGlvbi1kZW1vIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImUwZDgxNzk5OTFkZDczNWJhZjA5NjE5MDFiMzM0NzZhNzZhMGYzMDBiYzRlYTA3ZTNkN2FlN2MyNGUxNDcxOTMifX1dLCJwcmVkaWNhdGUiOnsibWV0YWRhdGEiOnsibmFtZSI6ImJ1aWxkLWFuZC1yZWxlYXNlIiwicHJvamVjdCI6ImludGVncmF0aW9uLWRlbW8iLCJ0ZWFtIjoiIiwiaW5pdGlhbGl6ZWRBdCI6IjIwMjMtMDMtMTNUMjM6MzU6MzYuOTgzMTM4OTE5WiIsImZpbmlzaGVkQXQiOiIyMDIzLTAzLTEzVDIzOjM2OjM2Ljc0OTMyNTEwM1oiLCJ3b3JrZmxvd1J1bklEIjoiOTEwOTNmNmUtMzdjMC00ZTAyLWFmNWQtYzk4NTk1OTZkMTI1Iiwid29ya2Zsb3dJRCI6IjM3MDIyYjJmLTM0YzMtNGY0Ny05ZmQ3LTUxNGI0YTdiYWFhZCJ9LCJtYXRlcmlhbHMiOlt7Im5hbWUiOiJiaW5hcnkiLCJ0eXBlIjoiQVJUSUZBQ1QiLCJtYXRlcmlhbCI6eyJzbHNhIjp7InVyaSI6ImludGVncmF0aW9uLWRlbW9fMC4wLjM5X2xpbnV4X2FtZDY0LnRhci5neiIsImRpZ2VzdCI6eyJzaGEyNTYiOiJiMTU1Y2RmYzMyOGIyNzNjNGI3NDFjMDhiM2I4NGFjNDQxYjA1NjJjYTUxODkzZjIzNDk1YjM1YWJmODllYTg3In19fX0seyJuYW1lIjoiaW1hZ2UiLCJ0eXBlIjoiQ09OVEFJTkVSX0lNQUdFIiwibWF0ZXJpYWwiOnsic2xzYSI6eyJ1cmkiOiJnaGNyLmlvL2NoYWlubG9vcC1kZXYvaW50ZWdyYXRpb24tZGVtbyIsImRpZ2VzdCI6eyJzaGEyNTYiOiJlMGQ4MTc5OTkxZGQ3MzViYWYwOTYxOTAxYjMzNDc2YTc2YTBmMzAwYmM0ZWEwN2UzZDdhZTdjMjRlMTQ3MTkzIn19fX0seyJuYW1lIjoic2JvbSIsInR5cGUiOiJTQk9NX0NZQ0xPTkVEWF9KU09OIiwibWF0ZXJpYWwiOnsic2xzYSI6eyJ1cmkiOiJzYm9tLmN5Y2xvbmVkeC5qc29uIiwiZGlnZXN0Ijp7InNoYTI1NiI6ImI1MGYzODk2MWNjMmU5N2QwOTAzZjQ2ODNhNDBlMjUyOGY3ZjZjOWQzODJlOGM2MDQ4YjAzNjNhZjk1YjcwODAifX19fV0sImJ1aWxkZXIiOnsiaWQiOiJjaGFpbmxvb3AuZGV2L2NsaS8wLjguOTJAc2hhMjU2OmZhMDFjNmNkMTA0ODAzZWQ1YjFlODBlMjE2YzFjN2Y2MjQ0YjIxY2VmODVjYTQ4YmI2OTMwN2JhMTcxM2U2MTkifSwiYnVpbGRUeXBlIjoiY2hhaW5sb29wLmRldi93b3JrZmxvd3J1bi92MC4xIiwiZW52Ijp7IkdJVEhVQl9BQ1RPUiI6Im1pZ21hcnRyaSIsIkdJVEhVQl9SRUYiOiJyZWZzL3RhZ3MvdjAuMC4zOSIsIkdJVEhVQl9SRVBPU0lUT1JZIjoiY2hhaW5sb29wLWRldi9pbnRlZ3JhdGlvbi1kZW1vIiwiR0lUSFVCX1JFUE9TSVRPUllfT1dORVIiOiJjaGFpbmxvb3AtZGV2IiwiR0lUSFVCX1JVTl9JRCI6IjQ0MTA1NDMzNjUiLCJHSVRIVUJfU0hBIjoiMGFjY2M5MzkyZmIxZjliMjU4MTY3YzE4ZmZhMGFlYjYyNjk3M2YxYyIsIlJVTk5FUl9OQU1FIjoiSG9zdGVkIEFnZW50IiwiUlVOTkVSX09TIjoiTGludXgifSwicnVubmVyVHlwZSI6IkdJVEhVQl9BQ1RJT04iLCJydW5uZXJVUkwiOiJodHRwczovL2dpdGh1Yi5jb20vY2hhaW5sb29wLWRldi9pbnRlZ3JhdGlvbi1kZW1vL2FjdGlvbnMvcnVucy80NDEwNTQzMzY1In19", + "signatures": [ + { + "keyid": "", + "sig": "MEQCIBlSdPt604OSyDMF4vjY8DoKQ6uxc1NADtay0q4Ii4f7AiA4KxaiIc1HPSi0a0bJv3l0V/wR2aPvxsDDfmAEd+LFTA==" + } + ] +} diff --git a/internal/attestation/renderer/testdata/valid.predicate.json b/internal/attestation/renderer/testdata/valid.predicate.json new file mode 100644 index 000000000..ac0c37881 --- /dev/null +++ b/internal/attestation/renderer/testdata/valid.predicate.json @@ -0,0 +1,65 @@ +{ + "buildType": "chainloop.dev/workflowrun/v0.1", + "builder": { + "id": "chainloop.dev/cli/0.8.92@sha256:fa01c6cd104803ed5b1e80e216c1c7f6244b21cef85ca48bb69307ba1713e619" + }, + "env": { + "GITHUB_ACTOR": "migmartri", + "GITHUB_REF": "refs/tags/v0.0.39", + "GITHUB_REPOSITORY": "chainloop-dev/integration-demo", + "GITHUB_REPOSITORY_OWNER": "chainloop-dev", + "GITHUB_RUN_ID": "4410543365", + "GITHUB_SHA": "0accc9392fb1f9b258167c18ffa0aeb626973f1c", + "RUNNER_NAME": "Hosted Agent", + "RUNNER_OS": "Linux" + }, + "materials": [ + { + "material": { + "slsa": { + "digest": { + "sha256": "b155cdfc328b273c4b741c08b3b84ac441b0562ca51893f23495b35abf89ea87" + }, + "uri": "integration-demo_0.0.39_linux_amd64.tar.gz" + } + }, + "name": "binary", + "type": "ARTIFACT" + }, + { + "material": { + "slsa": { + "digest": { + "sha256": "e0d8179991dd735baf0961901b33476a76a0f300bc4ea07e3d7ae7c24e147193" + }, + "uri": "ghcr.io/chainloop-dev/integration-demo" + } + }, + "name": "image", + "type": "CONTAINER_IMAGE" + }, + { + "material": { + "slsa": { + "digest": { + "sha256": "b50f38961cc2e97d0903f4683a40e2528f7f6c9d382e8c6048b0363af95b7080" + }, + "uri": "sbom.cyclonedx.json" + } + }, + "name": "sbom", + "type": "SBOM_CYCLONEDX_JSON" + } + ], + "metadata": { + "finishedAt": "2023-03-13T23:36:36.749325103Z", + "initializedAt": "2023-03-13T23:35:36.983138919Z", + "name": "build-and-release", + "project": "integration-demo", + "team": "", + "workflowID": "37022b2f-34c3-4f47-9fd7-514b4a7baaad", + "workflowRunID": "91093f6e-37c0-4e02-af5d-c9859596d125" + }, + "runnerType": "GITHUB_ACTION", + "runnerURL": "https://github.com/chainloop-dev/integration-demo/actions/runs/4410543365" +} \ No newline at end of file