Skip to content

Commit fe7efe0

Browse files
authored
feat(verification): attestation verify command (chainloop-dev#1814)
Signed-off-by: Jose I. Paris <[email protected]>
1 parent 353cbaf commit fe7efe0

File tree

4 files changed

+135
-25
lines changed

4 files changed

+135
-25
lines changed

app/cli/cmd/attestation.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,8 @@ func newAttestationCmd() *cobra.Command {
7272
cmd.PersistentFlags().BoolVar(&GracefulExit, "graceful-exit", false, "exit 0 in case of error. NOTE: this flag will be removed once Chainloop reaches 1.0")
7373
cmd.PersistentFlags().StringVar(&attestationLocalStatePath, "local-state-path", "", "path to store the attestation state locally, default: [tmpDir]/chainloop_attestation.tmp.json")
7474

75-
cmd.AddCommand(newAttestationInitCmd(), newAttestationAddCmd(), newAttestationStatusCmd(), newAttestationPushCmd(), newAttestationResetCmd())
75+
cmd.AddCommand(newAttestationInitCmd(), newAttestationAddCmd(), newAttestationStatusCmd(), newAttestationPushCmd(),
76+
newAttestationResetCmd(), newAttestationVerifyCmd())
7677

7778
return cmd
7879
}

app/cli/cmd/attestation_verify.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//
2+
// Copyright 2025 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 cmd
17+
18+
import (
19+
"fmt"
20+
21+
"github.com/chainloop-dev/chainloop/app/cli/internal/action"
22+
"github.com/spf13/cobra"
23+
)
24+
25+
func newAttestationVerifyCmd() *cobra.Command {
26+
var fileOrURL string
27+
cmd := &cobra.Command{
28+
Use: "verify file-or-url",
29+
Short: "verify an attestation",
30+
Long: "Verify an attestation by validating its validation material against the configured trusted root",
31+
DisableFlagsInUseLine: true,
32+
Example: ` # verify local attestation
33+
chainloop attestation verify --bundle attestation.json
34+
35+
# verify an attestation stored in an https endpoint
36+
chainloop attestation verify -b https://myrepository/attestation.json`,
37+
RunE: func(cmd *cobra.Command, _ []string) error {
38+
res, err := action.NewAttestationVerifyAction(actionOpts).Run(cmd.Context(), fileOrURL)
39+
if err != nil {
40+
return fmt.Errorf("verifying attestation: %w", err)
41+
}
42+
if res {
43+
actionOpts.Logger.Info().Msg("attestation verified successfully")
44+
} else {
45+
actionOpts.Logger.Warn().Msg("attestation couldn't be verified")
46+
}
47+
48+
return nil
49+
},
50+
}
51+
52+
cmd.Flags().StringVarP(&fileOrURL, "bundle", "b", "", "bundle path or URL")
53+
cobra.CheckErr(cmd.MarkFlagRequired("bundle"))
54+
55+
return cmd
56+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
//
2+
// Copyright 2025 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 action
17+
18+
import (
19+
"context"
20+
"errors"
21+
"fmt"
22+
23+
pb "github.com/chainloop-dev/chainloop/app/controlplane/api/controlplane/v1"
24+
"github.com/chainloop-dev/chainloop/pkg/attestation/verifier"
25+
"github.com/sigstore/cosign/v2/pkg/blob"
26+
"google.golang.org/grpc/codes"
27+
"google.golang.org/grpc/status"
28+
)
29+
30+
type AttestationVerifyAction struct {
31+
cfg *ActionsOpts
32+
}
33+
34+
func NewAttestationVerifyAction(cfg *ActionsOpts) *AttestationVerifyAction {
35+
return &AttestationVerifyAction{cfg}
36+
}
37+
38+
func (action *AttestationVerifyAction) Run(ctx context.Context, fileOrURL string) (bool, error) {
39+
content, err := blob.LoadFileOrURL(fileOrURL)
40+
if err != nil {
41+
return false, fmt.Errorf("loading attestation: %w", err)
42+
}
43+
44+
return verifyBundle(ctx, content, action.cfg)
45+
}
46+
47+
func verifyBundle(ctx context.Context, content []byte, opts *ActionsOpts) (bool, error) {
48+
sc := pb.NewSigningServiceClient(opts.CPConnection)
49+
trResp, err := sc.GetTrustedRoot(ctx, &pb.GetTrustedRootRequest{})
50+
if err != nil {
51+
// if trusted root is not implemented, skip verification
52+
if status.Code(err) != codes.Unimplemented {
53+
return false, fmt.Errorf("failed getting trusted root: %w", err)
54+
}
55+
}
56+
57+
if trResp != nil {
58+
tr, err := trustedRootPbToVerifier(trResp)
59+
if err != nil {
60+
return false, fmt.Errorf("getting roots: %w", err)
61+
}
62+
if err = verifier.VerifyBundle(ctx, content, tr); err != nil {
63+
if !errors.Is(err, verifier.ErrMissingVerificationMaterial) {
64+
opts.Logger.Debug().Err(err).Msg("bundle verification failed")
65+
return false, errors.New("bundle verification failed")
66+
}
67+
} else {
68+
return true, nil
69+
}
70+
}
71+
72+
return false, nil
73+
}

app/cli/internal/action/workflow_run_describe.go

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,6 @@ import (
3535
"github.com/sigstore/sigstore/pkg/cryptoutils"
3636
"github.com/sigstore/sigstore/pkg/signature"
3737
sigdsee "github.com/sigstore/sigstore/pkg/signature/dsse"
38-
"google.golang.org/grpc/codes"
39-
"google.golang.org/grpc/status"
4038
)
4139

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

174172
if att.Bundle != nil {
175-
sc := pb.NewSigningServiceClient(action.cfg.CPConnection)
176-
trResp, err := sc.GetTrustedRoot(ctx, &pb.GetTrustedRootRequest{})
173+
res, err := verifyBundle(ctx, att.Bundle, action.cfg)
177174
if err != nil {
178-
// if trusted root is not implemented, skip verification
179-
if status.Code(err) != codes.Unimplemented {
180-
return nil, fmt.Errorf("failed getting trusted root: %w", err)
181-
}
182-
}
183-
184-
if trResp != nil {
185-
tr, err := trustedRootPbToVerifier(trResp)
186-
if err != nil {
187-
return nil, fmt.Errorf("getting roots: %w", err)
188-
}
189-
if err = verifier.VerifyBundle(ctx, att.Bundle, tr); err != nil {
190-
if !errors.Is(err, verifier.ErrMissingVerificationMaterial) {
191-
action.cfg.Logger.Debug().Err(err).Msg("bundle verification failed")
192-
return nil, errors.New("bundle verification failed")
193-
}
194-
} else {
195-
item.Verified = true
196-
}
175+
return nil, fmt.Errorf("bundle verification failed: %w", err)
197176
}
177+
item.Verified = res
198178
}
199179

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

0 commit comments

Comments
 (0)