Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cmd/ratify/root/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func New() *cobra.Command {
}
cmd.AddCommand(
versionCommand(),
verifyCommand(nil),
)
return cmd
}
220 changes: 220 additions & 0 deletions cmd/ratify/root/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
// Copyright The Ratify 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 root

import (
"context"
"encoding/json"
"errors"
"fmt"
"os"

"github.com/ratify-project/ratify-cli/v2/internal/verifier"
"github.com/ratify-project/ratify-go"
"github.com/ratify-project/ratify-verifier-go/notation"
"github.com/spf13/cobra"
"oras.land/oras-go/v2/registry"
)

type verifyOptions struct {
subject string
configFilePath string
storePath string
}

type validationResult struct {
Succeeded bool `json:"succeeded"`
ArtifactReports []validationReport `json:"artifactReports"`
}

type validationReport struct {
Subject string `json:"subject"`
Artifact string `json:"artifact"`
ArtifactType string `json:"artifactType"`
Results []verificationResult `json:"results"`
ArtifactReports []validationReport `json:"artifactReports"`
}

type verificationResult struct {
Succeeded bool `json:"succeeded"`
Description string `json:"description,omitempty"`
VerifierName string `json:"verifierName"`
VerifierType string `json:"verifierType"`
Detail any `json:"detail,omitempty"`
}

func verifyCommand(opts *verifyOptions) *cobra.Command {
if opts == nil {
opts = &verifyOptions{}
}
longMessage := `Verify an artifact
Prerequisite: added a trust store for notation verifier and an OCI store saving artifacts.

Example - Verify an artifact:
ratify verify --subject <subject> --config <config file> --store <store path>
`

cmd := &cobra.Command{
Use: "verify",
Short: "Verify the provided artifact",
Long: longMessage,
Args: cobra.NoArgs,
RunE: func(cmd *cobra.Command, args []string) error {
return runVerify(cmd, opts)
},
}

flags := cmd.Flags()
flags.StringVarP(&opts.subject, "subject", "s", "", "subject to verify")
flags.StringVarP(&opts.configFilePath, "config", "c", "", "path to the verifier config file")
flags.StringVarP(&opts.storePath, "store", "t", "", "path to the store")
return cmd
}

func runVerify(cmd *cobra.Command, opts *verifyOptions) error {
if opts.subject == "" {
return errors.New("subject is required")
}
ref, err := registry.ParseReference(opts.subject)
if err != nil {
return fmt.Errorf("Invalid subject reference: %s, err: %w", opts.subject, err)
}
repo := ref.Registry + "/" + ref.Repository

store, err := createStore(cmd.Context(), opts.storePath, repo)
if err != nil {
return err
}

verifiers, err := loadNotationVerifiersFromConfigFile(opts.configFilePath)
if err != nil {
return fmt.Errorf("Error loading verifiers: %v", err)
}

executor, err := ratify.NewExecutor(store, verifiers, nil)
if err != nil {
return fmt.Errorf("Error creating executor: %v", err)
}

validateOpts := ratify.ValidateArtifactOptions{
Subject: opts.subject,
}
result, err := executor.ValidateArtifact(cmd.Context(), validateOpts)
if err != nil {
return fmt.Errorf("Error validating artifact: %v", err)
}

printReport(result, repo)
return nil
}

func createStore(ctx context.Context, storePath, repo string) (*ratify.StoreMux, error) {
fileInfo, err := os.Stat(storePath)
if os.IsNotExist(err) {
return nil, errors.New("store path does not exist")
}
if err != nil {
return nil, fmt.Errorf("Error checking store path: %v", err)
}

var store *ratify.OCIStore
if fileInfo.IsDir() {
fs := os.DirFS(storePath)
store, err = ratify.NewOCIStoreFromFS(ctx, "store", fs)
if err != nil {
return nil, fmt.Errorf("Error creating store from FS: %v", err)
}
} else {
store, err = ratify.NewOCIStoreFromTar(ctx, "store", storePath)
if err != nil {
return nil, fmt.Errorf("Error creating store from tar: %v", err)
}
}

mux := ratify.NewStoreMux("multiplexer")
if err := mux.Register(repo, store); err != nil {
return nil, fmt.Errorf("Error registering store: %v", err)
}
return mux, nil
}

func loadNotationVerifiersFromConfigFile(configFilePath string) ([]ratify.Verifier, error) {
body, err := os.ReadFile(configFilePath)
if err != nil {
return nil, fmt.Errorf("Error reading config file: %w", err)
}

verifierOpts, err := verifier.ParseNotationVerifierOptions(body)
if err != nil {
return nil, fmt.Errorf("Error loading verifier options: %w", err)
}

verifier, err := notation.NewVerifier(verifierOpts)
if err != nil {
return nil, fmt.Errorf("Error creating verifier: %w", err)
}

return []ratify.Verifier{verifier}, nil
}

func printReport(result *ratify.ValidationResult, repo string) {
convertedResult := convertValidationResult(result, repo)
jsonData, err := json.MarshalIndent(convertedResult, "", " ")
if err != nil {
fmt.Println("Error marshalling to JSON:", err)
return
}
fmt.Println(string(jsonData))
}

func convertValidationResult(result *ratify.ValidationResult, repo string) *validationResult {
return &validationResult{
Succeeded: result.Succeeded,
ArtifactReports: convertValidationReports(result.ArtifactReports, repo),
}
}

func convertValidationReports(reports []*ratify.ValidationReport, repo string) []validationReport {
var convertedReports []validationReport
for _, report := range reports {
convertedReport := validationReport{
Subject: report.Subject,
Artifact: repo + "@" + report.Artifact.Digest.String(),
ArtifactType: report.Artifact.ArtifactType,
Results: convertVerificationResults(report.Results),
ArtifactReports: convertValidationReports(report.ArtifactReports, repo),
}
convertedReports = append(convertedReports, convertedReport)
}
return convertedReports
}

func convertVerificationResults(results []*ratify.VerificationResult) []verificationResult {
var convertedResults []verificationResult
for _, result := range results {
detail := result.Detail
if result.Err != nil {
detail = result.Err.Error()
}
convertedResult := verificationResult{
Succeeded: result.Err == nil,
Description: result.Description,
VerifierName: result.Verifier.Name(),
VerifierType: result.Verifier.Type(),
Detail: detail,
}
convertedResults = append(convertedResults, convertedResult)
}
return convertedResults
}
24 changes: 23 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,31 @@ module github.com/ratify-project/ratify-cli/v2

go 1.23.1

require github.com/spf13/cobra v1.8.1
require (
github.com/notaryproject/notation-go v1.3.0
github.com/ratify-project/ratify-go v0.0.0-20250206022400-b18f90180052
github.com/ratify-project/ratify-verifier-go/notation v0.0.0-20250206033055-cc0bd9b73c33
github.com/spf13/cobra v1.8.1
)

require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.7 // indirect
github.com/go-ldap/ldap/v3 v3.4.10 // indirect
github.com/golang-jwt/jwt/v4 v4.5.1 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/notaryproject/notation-core-go v1.2.0 // indirect
github.com/notaryproject/notation-plugin-framework-go v1.0.0 // indirect
github.com/notaryproject/tspclient-go v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/veraison/go-cose v1.3.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
golang.org/x/crypto v0.32.0 // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/sync v0.10.0 // indirect
oras.land/oras-go/v2 v2.5.0 // indirect
)
Loading
Loading