diff --git a/app/cli/internal/action/workflow_run_describe.go b/app/cli/internal/action/workflow_run_describe.go index d20d3fbc3..ffb2e3f6e 100644 --- a/app/cli/internal/action/workflow_run_describe.go +++ b/app/cli/internal/action/workflow_run_describe.go @@ -22,18 +22,21 @@ import ( "errors" "fmt" "sort" + "strings" pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1" "github.com/chainloop-dev/chainloop/pkg/attestation/renderer/chainloop" + "github.com/chainloop-dev/chainloop/pkg/attestation/verifier" + intoto "github.com/in-toto/attestation/go/v1" + "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/sigstore/cosign/v2/pkg/blob" "github.com/sigstore/cosign/v2/pkg/cosign" sigs "github.com/sigstore/cosign/v2/pkg/signature" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" - - intoto "github.com/in-toto/attestation/go/v1" - "github.com/secure-systems-lab/go-securesystemslib/dsse" sigdsee "github.com/sigstore/sigstore/pkg/signature/dsse" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) type WorkflowRunDescribe struct { @@ -157,17 +160,43 @@ func (action *WorkflowRunDescribe) Run(ctx context.Context, opts *WorkflowRunDes item.WorkflowRun.FinishedAt = toTimePtr(wr.FinishedAt.AsTime()) } - attestation := resp.GetResult().GetAttestation() + att := resp.GetResult().GetAttestation() // The item does not have associated attestation - if attestation == nil { + if att == nil { return item, nil } - envelope, err := decodeEnvelope(attestation.Envelope) + envelope, err := decodeEnvelope(att.Envelope) if err != nil { return nil, err } + if att.Bundle != nil { + sc := pb.NewSigningServiceClient(action.cfg.CPConnection) + trResp, err := sc.GetTrustedRoot(ctx, &pb.GetTrustedRootRequest{}) + if err != nil { + // if trusted root is not implemented, skip verification + if status.Code(err) != codes.Unimplemented { + return nil, fmt.Errorf("failed getting trusted root: %w", err) + } + } + + if trResp != nil { + tr, err := trustedRootPbToVerifier(trResp) + if err != nil { + return nil, fmt.Errorf("getting roots: %w", err) + } + if err = verifier.VerifyBundle(ctx, att.Bundle, tr); err != nil { + if !errors.Is(err, verifier.ErrMissingVerificationMaterial) { + action.cfg.Logger.Debug().Err(err).Msg("bundle verification failed") + return nil, errors.New("bundle verification failed") + } + } else { + item.Verified = true + } + } + } + if opts.Verify { if err := verifyEnvelope(ctx, envelope, opts); err != nil { action.cfg.Logger.Debug().Err(err).Msg("verifying the envelope") @@ -182,31 +211,31 @@ func (action *WorkflowRunDescribe) Run(ctx context.Context, opts *WorkflowRunDes return nil, fmt.Errorf("extracting statement: %w", err) } - envVars := make([]*EnvVar, 0, len(attestation.GetEnvVars())) - for _, v := range attestation.GetEnvVars() { + envVars := make([]*EnvVar, 0, len(att.GetEnvVars())) + for _, v := range att.GetEnvVars() { envVars = append(envVars, &EnvVar{Name: v.Name, Value: v.Value}) } - materials := make([]*Material, 0, len(attestation.GetMaterials())) - for _, v := range attestation.GetMaterials() { + materials := make([]*Material, 0, len(att.GetMaterials())) + for _, v := range att.GetMaterials() { materials = append(materials, materialPBToAction(v)) } - keys := make([]string, 0, len(attestation.GetAnnotations())) - for k := range attestation.GetAnnotations() { + keys := make([]string, 0, len(att.GetAnnotations())) + for k := range att.GetAnnotations() { keys = append(keys, k) } sort.Strings(keys) - annotations := make([]*Annotation, 0, len(attestation.GetAnnotations())) + annotations := make([]*Annotation, 0, len(att.GetAnnotations())) for _, k := range keys { annotations = append(annotations, &Annotation{ - Name: k, Value: attestation.GetAnnotations()[k], + Name: k, Value: att.GetAnnotations()[k], }) } evaluations := make(map[string][]*PolicyEvaluation) - for k, v := range attestation.GetPolicyEvaluations() { + for k, v := range att.GetPolicyEvaluations() { evs := make([]*PolicyEvaluation, 0) for _, ev := range v.Evaluations { evs = append(evs, policyEvaluationPBToAction(ev)) @@ -214,16 +243,16 @@ func (action *WorkflowRunDescribe) Run(ctx context.Context, opts *WorkflowRunDes evaluations[k] = evs } - policyEvaluationStatus := attestation.GetPolicyEvaluationStatus() + policyEvaluationStatus := att.GetPolicyEvaluationStatus() item.Attestation = &WorkflowRunAttestationItem{ Envelope: envelope, - Bundle: attestation.GetBundle(), + Bundle: att.GetBundle(), statement: statement, EnvVars: envVars, Materials: materials, Annotations: annotations, - Digest: attestation.DigestInCasBackend, + Digest: att.DigestInCasBackend, PolicyEvaluations: evaluations, PolicyEvaluationStatus: &PolicyEvaluationStatus{ Strategy: policyEvaluationStatus.Strategy, @@ -236,6 +265,20 @@ func (action *WorkflowRunDescribe) Run(ctx context.Context, opts *WorkflowRunDes return item, nil } +func trustedRootPbToVerifier(resp *pb.GetTrustedRootResponse) (*verifier.TrustedRoot, error) { + tr := &verifier.TrustedRoot{Keys: make(map[string][]*x509.Certificate)} + for k, v := range resp.GetKeys() { + for _, c := range v.Certificates { + cert, err := cryptoutils.LoadCertificatesFromPEM(strings.NewReader(c)) + if err != nil { + return nil, fmt.Errorf("loading certificate from PEM: %w", err) + } + tr.Keys[k] = append(tr.Keys[k], cert[0]) + } + } + return tr, nil +} + func policyEvaluationPBToAction(in *pb.PolicyEvaluation) *PolicyEvaluation { var pr *PolicyReference if in.PolicyReference != nil { diff --git a/go.mod b/go.mod index 5a009c55c..1bdc70ccd 100644 --- a/go.mod +++ b/go.mod @@ -85,6 +85,7 @@ require ( github.com/santhosh-tekuri/jsonschema/v5 v5.3.1 github.com/sigstore/fulcio v1.6.3 github.com/sigstore/protobuf-specs v0.3.2 + github.com/sigstore/sigstore-go v0.6.1 github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.8 github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.8 github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.8 @@ -185,6 +186,7 @@ require ( github.com/spiffe/go-spiffe/v2 v2.3.0 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect + github.com/theupdateframework/go-tuf/v2 v2.0.1 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect github.com/tklauser/numcpus v0.9.0 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect diff --git a/go.sum b/go.sum index df9587bc9..5c8a8e123 100644 --- a/go.sum +++ b/go.sum @@ -800,6 +800,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl v1.0.1-vault-5 h1:kI3hhbbyzr4dldA8UdTb7ZlVVlI2DACdCfz31RPDgJM= github.com/hashicorp/hcl v1.0.1-vault-5/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM= diff --git a/pkg/attestation/verifier/verifier.go b/pkg/attestation/verifier/verifier.go new file mode 100644 index 000000000..d8942a90d --- /dev/null +++ b/pkg/attestation/verifier/verifier.go @@ -0,0 +1,83 @@ +// +// Copyright 2025 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 verifier + +import ( + "context" + "crypto/sha256" + "crypto/x509" + "errors" + "fmt" + + "github.com/chainloop-dev/chainloop/pkg/attestation" + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/cosign/v2/pkg/cosign" + protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" + sigstorebundle "github.com/sigstore/sigstore-go/pkg/bundle" + sigdsee "github.com/sigstore/sigstore/pkg/signature/dsse" + "google.golang.org/protobuf/encoding/protojson" +) + +type TrustedRoot struct { + // map key identifiers to a chain of certificates + Keys map[string][]*x509.Certificate +} + +var ErrMissingVerificationMaterial = errors.New("missing material") + +func VerifyBundle(ctx context.Context, bundleBytes []byte, tr *TrustedRoot) error { + bundle := new(protobundle.Bundle) + // unmarshal and validate + if err := protojson.Unmarshal(bundleBytes, bundle); err != nil { + return fmt.Errorf("invalid bundle: %w", err) + } + + if bundle.GetVerificationMaterial() == nil || bundle.GetVerificationMaterial().GetCertificate() == nil { + // nothing to verify + return ErrMissingVerificationMaterial + } + + // Use sigstore helpers + var sb sigstorebundle.Bundle + if err := sb.UnmarshalJSON(bundleBytes); err != nil { + return fmt.Errorf("invalid bundle: %w", err) + } + + vc, err := sb.VerificationContent() + if err != nil { + return fmt.Errorf("could not get verification material: %w", err) + } + signingCert := vc.GetCertificate() + + aki := fmt.Sprintf("%x", sha256.Sum256(signingCert.AuthorityKeyId)) + chain, ok := tr.Keys[aki] + if !ok { + return fmt.Errorf("trusted root not found for signing key with AKI %s", aki) + } + + verifier, err := cosign.ValidateAndUnpackCertWithChain(signingCert, chain, &cosign.CheckOpts{IgnoreSCT: true}) + if err != nil { + return fmt.Errorf("validating the certificate: %w", err) + } + + dsseVerifier, err := dsse.NewEnvelopeVerifier(&sigdsee.VerifierAdapter{SignatureVerifier: verifier}) + if err != nil { + return fmt.Errorf("creating DSSE verifier: %w", err) + } + + _, err = dsseVerifier.Verify(ctx, attestation.DSSEEnvelopeFromBundle(bundle)) + return err +}