Skip to content

Commit

Permalink
feat(verification): attestation verify command (chainloop-dev#1814)
Browse files Browse the repository at this point in the history
Signed-off-by: Jose I. Paris <[email protected]>
  • Loading branch information
jiparis authored Feb 17, 2025
1 parent 353cbaf commit fe7efe0
Show file tree
Hide file tree
Showing 4 changed files with 135 additions and 25 deletions.
3 changes: 2 additions & 1 deletion app/cli/cmd/attestation.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ func newAttestationCmd() *cobra.Command {
cmd.PersistentFlags().BoolVar(&GracefulExit, "graceful-exit", false, "exit 0 in case of error. NOTE: this flag will be removed once Chainloop reaches 1.0")
cmd.PersistentFlags().StringVar(&attestationLocalStatePath, "local-state-path", "", "path to store the attestation state locally, default: [tmpDir]/chainloop_attestation.tmp.json")

cmd.AddCommand(newAttestationInitCmd(), newAttestationAddCmd(), newAttestationStatusCmd(), newAttestationPushCmd(), newAttestationResetCmd())
cmd.AddCommand(newAttestationInitCmd(), newAttestationAddCmd(), newAttestationStatusCmd(), newAttestationPushCmd(),
newAttestationResetCmd(), newAttestationVerifyCmd())

return cmd
}
Expand Down
56 changes: 56 additions & 0 deletions app/cli/cmd/attestation_verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//
// 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 cmd

import (
"fmt"

"github.com/chainloop-dev/chainloop/app/cli/internal/action"
"github.com/spf13/cobra"
)

func newAttestationVerifyCmd() *cobra.Command {
var fileOrURL string
cmd := &cobra.Command{
Use: "verify file-or-url",
Short: "verify an attestation",
Long: "Verify an attestation by validating its validation material against the configured trusted root",
DisableFlagsInUseLine: true,
Example: ` # verify local attestation
chainloop attestation verify --bundle attestation.json
# verify an attestation stored in an https endpoint
chainloop attestation verify -b https://myrepository/attestation.json`,
RunE: func(cmd *cobra.Command, _ []string) error {
res, err := action.NewAttestationVerifyAction(actionOpts).Run(cmd.Context(), fileOrURL)
if err != nil {
return fmt.Errorf("verifying attestation: %w", err)
}
if res {
actionOpts.Logger.Info().Msg("attestation verified successfully")
} else {
actionOpts.Logger.Warn().Msg("attestation couldn't be verified")
}

return nil
},
}

cmd.Flags().StringVarP(&fileOrURL, "bundle", "b", "", "bundle path or URL")
cobra.CheckErr(cmd.MarkFlagRequired("bundle"))

return cmd
}
73 changes: 73 additions & 0 deletions app/cli/internal/action/attestation_verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//
// 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 action

import (
"context"
"errors"
"fmt"

pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
"github.com/chainloop-dev/chainloop/pkg/attestation/verifier"
"github.com/sigstore/cosign/v2/pkg/blob"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type AttestationVerifyAction struct {
cfg *ActionsOpts
}

func NewAttestationVerifyAction(cfg *ActionsOpts) *AttestationVerifyAction {
return &AttestationVerifyAction{cfg}
}

func (action *AttestationVerifyAction) Run(ctx context.Context, fileOrURL string) (bool, error) {
content, err := blob.LoadFileOrURL(fileOrURL)
if err != nil {
return false, fmt.Errorf("loading attestation: %w", err)
}

return verifyBundle(ctx, content, action.cfg)
}

func verifyBundle(ctx context.Context, content []byte, opts *ActionsOpts) (bool, error) {
sc := pb.NewSigningServiceClient(opts.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 false, fmt.Errorf("failed getting trusted root: %w", err)
}
}

if trResp != nil {
tr, err := trustedRootPbToVerifier(trResp)
if err != nil {
return false, fmt.Errorf("getting roots: %w", err)
}
if err = verifier.VerifyBundle(ctx, content, tr); err != nil {
if !errors.Is(err, verifier.ErrMissingVerificationMaterial) {
opts.Logger.Debug().Err(err).Msg("bundle verification failed")
return false, errors.New("bundle verification failed")
}
} else {
return true, nil
}
}

return false, nil
}
28 changes: 4 additions & 24 deletions app/cli/internal/action/workflow_run_describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,6 @@ import (
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/sigstore/sigstore/pkg/signature"
sigdsee "github.com/sigstore/sigstore/pkg/signature/dsse"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type WorkflowRunDescribe struct {
Expand Down Expand Up @@ -172,32 +170,14 @@ func (action *WorkflowRunDescribe) Run(ctx context.Context, opts *WorkflowRunDes
}

if att.Bundle != nil {
sc := pb.NewSigningServiceClient(action.cfg.CPConnection)
trResp, err := sc.GetTrustedRoot(ctx, &pb.GetTrustedRootRequest{})
res, err := verifyBundle(ctx, att.Bundle, action.cfg)
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
}
return nil, fmt.Errorf("bundle verification failed: %w", err)
}
item.Verified = res
}

if opts.Verify {
if opts.Verify && !item.Verified {
if err := verifyEnvelope(ctx, envelope, opts); err != nil {
action.cfg.Logger.Debug().Err(err).Msg("verifying the envelope")
return nil, errors.New("invalid signature, did you provide the right key?")
Expand Down

0 comments on commit fe7efe0

Please sign in to comment.