...

Source file src/github.com/sigstore/timestamp-authority/cmd/timestamp-cli/app/verify.go

Documentation: github.com/sigstore/timestamp-authority/cmd/timestamp-cli/app

     1  //
     2  // Copyright 2022 The Sigstore 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 app
    17  
    18  import (
    19  	"crypto/x509"
    20  	"fmt"
    21  	"math/big"
    22  	"os"
    23  	"path/filepath"
    24  	"strconv"
    25  	"strings"
    26  
    27  	"github.com/digitorus/timestamp"
    28  	"github.com/sigstore/sigstore/pkg/cryptoutils"
    29  	"github.com/sigstore/timestamp-authority/cmd/timestamp-cli/app/format"
    30  	"github.com/sigstore/timestamp-authority/pkg/log"
    31  	"github.com/sigstore/timestamp-authority/pkg/verification"
    32  	"github.com/spf13/cobra"
    33  	"github.com/spf13/viper"
    34  )
    35  
    36  type verifyCmdOutput struct {
    37  	TimestampPath   string
    38  	ParsedTimestamp timestamp.Timestamp
    39  }
    40  
    41  func (v *verifyCmdOutput) String() string {
    42  	return fmt.Sprintf("Successfully verified timestamp %s", v.TimestampPath)
    43  }
    44  
    45  func addVerifyFlags(cmd *cobra.Command) {
    46  	cmd.Flags().Var(NewFlagValue(fileFlag, ""), "artifact", "path to an blob with signed data")
    47  	cmd.MarkFlagRequired("artifact") //nolint:errcheck
    48  	cmd.Flags().Var(NewFlagValue(fileFlag, ""), "timestamp", "path to timestamp response to verify")
    49  	cmd.MarkFlagRequired("timestamp") //nolint:errcheck
    50  	cmd.Flags().Var(NewFlagValue(fileFlag, ""), "certificate-chain", "path to file with PEM-encoded certificate chain. Ordered from intermediate CA certificate that issued the TSA certificate, ending with the root CA certificate.")
    51  	cmd.Flags().String("nonce", "", "optional nonce passed with the request")
    52  	cmd.Flags().Var(NewFlagValue(oidFlag, ""), "oid", "optional TSA policy OID passed with the request")
    53  	cmd.Flags().String("common-name", "", "expected leaf certificate subject common name")
    54  	cmd.Flags().Var(NewFlagValue(fileFlag, ""), "certificate", "path to file with PEM-encoded leaf certificate")
    55  	cmd.Flags().Var(NewFlagValue(fileFlag, ""), "intermediate-certificates", "path to file with PEM-encoded intermediate certificates. Must be called with the root-certificate flag.")
    56  	cmd.Flags().Var(NewFlagValue(fileFlag, ""), "root-certificates", "path to file with a PEM-encoded root certificates. Optionally can be called with the intermediate-certificates flag.")
    57  }
    58  
    59  var verifyCmd = &cobra.Command{
    60  	Use:   "verify",
    61  	Short: "Verify timestamp",
    62  	Long:  "Verify the timestamp response using a timestamp certificate chain.",
    63  	PreRunE: func(cmd *cobra.Command, args []string) error {
    64  		if err := viper.BindPFlags(cmd.Flags()); err != nil {
    65  			log.CliLogger.Fatal("Error initializing cmd line args: ", err)
    66  		}
    67  		return nil
    68  	},
    69  	Run: format.WrapCmd(func(args []string) (interface{}, error) {
    70  		return runVerify()
    71  	}),
    72  }
    73  
    74  func runVerify() (interface{}, error) {
    75  	tsrPath := viper.GetString("timestamp")
    76  	tsrBytes, err := os.ReadFile(filepath.Clean(tsrPath))
    77  	if err != nil {
    78  		return nil, fmt.Errorf("error reading request from file: %w", err)
    79  	}
    80  
    81  	artifactPath := viper.GetString("artifact")
    82  	artifact, err := os.Open(filepath.Clean(artifactPath))
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  
    87  	opts, err := newVerifyOpts()
    88  	if err != nil {
    89  		return verifyCmdOutput{TimestampPath: tsrPath}, fmt.Errorf("failed to created VerifyOpts: %w", err)
    90  	}
    91  
    92  	ts, err := verification.VerifyTimestampResponse(tsrBytes, artifact, opts)
    93  	if err != nil {
    94  		return verifyCmdOutput{TimestampPath: tsrPath}, fmt.Errorf("failed to verify timestamp: %w", err)
    95  	}
    96  
    97  	return &verifyCmdOutput{TimestampPath: tsrPath, ParsedTimestamp: *ts}, nil
    98  }
    99  
   100  func newVerifyOpts() (verification.VerifyOpts, error) {
   101  	opts := verification.VerifyOpts{}
   102  
   103  	oid, err := getOID()
   104  	if err != nil {
   105  		return verification.VerifyOpts{}, fmt.Errorf("failed to parse value from oid flag: %w", err)
   106  	}
   107  	opts.OID = oid
   108  
   109  	certPathFlagVal := viper.GetString("certificate")
   110  	if certPathFlagVal != "" {
   111  		cert, err := parseTSACertificate(certPathFlagVal)
   112  		if err != nil {
   113  			return verification.VerifyOpts{}, fmt.Errorf("failed to parse cert flag value from PEM file: %w", err)
   114  		}
   115  		opts.TSACertificate = cert
   116  	}
   117  
   118  	roots, intermediates, err := getRootAndIntermediateCerts()
   119  	if err != nil {
   120  		return verification.VerifyOpts{}, fmt.Errorf("failed to parse root and intermediate certs from certificate-chain flag: %w", err)
   121  	}
   122  	opts.Roots = roots
   123  	opts.Intermediates = intermediates
   124  
   125  	nonce, err := getNonce()
   126  	if err != nil {
   127  		return verification.VerifyOpts{}, fmt.Errorf("failed to parse value from nonce flag: %w", err)
   128  	}
   129  	opts.Nonce = nonce
   130  
   131  	commonNameFlagVal := viper.GetString("common-name")
   132  	opts.CommonName = commonNameFlagVal
   133  
   134  	return opts, nil
   135  }
   136  
   137  func getNonce() (*big.Int, error) {
   138  	nonceFlagVal := viper.GetString("nonce")
   139  	if nonceFlagVal == "" {
   140  		return nil, nil
   141  	}
   142  
   143  	nonce := new(big.Int)
   144  	nonce, ok := nonce.SetString(nonceFlagVal, 10)
   145  	if !ok {
   146  		return nil, fmt.Errorf("failed to convert string to big.Int")
   147  	}
   148  	return nonce, nil
   149  }
   150  
   151  func getRootAndIntermediateCerts() ([]*x509.Certificate, []*x509.Certificate, error) {
   152  	certChainPEM := viper.GetString("certificate-chain")
   153  	rootPEM := viper.GetString("root-certificates")
   154  	intermediatePEM := viper.GetString("intermediate-certificates")
   155  
   156  	// the verify flag must be called with either one of the two flag combinations:
   157  	// 1. Called with the --root-certificates flag and optionally the --intermediate-certificates flag
   158  	// 2. Called with only the --certificate-chain flag
   159  
   160  	// this early exit if statement is only entered if neither of those combinations is valid
   161  	if !((rootPEM != "" && certChainPEM == "") || (intermediatePEM == "" && rootPEM == "" && certChainPEM != "")) {
   162  		return nil, nil, fmt.Errorf("the verify command must be called with either only the --certificate-chain flag or with the --root-certificates and --intermediate-certificates flags")
   163  	}
   164  
   165  	// return root and intermediate certificates when they've been passed
   166  	// together with the certificate-chain flag
   167  	if certChainPEM != "" {
   168  		pemBytes, err := os.ReadFile(filepath.Clean(certChainPEM))
   169  		if err != nil {
   170  			return nil, nil, fmt.Errorf("error reading request from file: %w", err)
   171  		}
   172  
   173  		certs, err := cryptoutils.UnmarshalCertificatesFromPEM(pemBytes)
   174  		if err != nil {
   175  			return nil, nil, fmt.Errorf("failed to parse intermediate and root certs from PEM file: %w", err)
   176  		}
   177  
   178  		if len(certs) == 0 {
   179  			return nil, nil, fmt.Errorf("expected at least one certificate to represent the root")
   180  		}
   181  
   182  		// intermediate certs are above the root certificate in the PEM file
   183  		intermediateCerts := certs[0 : len(certs)-1]
   184  		// the root certificate is last in the PEM file
   185  		rootCerts := []*x509.Certificate{certs[len(certs)-1]}
   186  
   187  		return rootCerts, intermediateCerts, nil
   188  	}
   189  
   190  	// return root and intermediate certificates when they've been passed
   191  	// separately with the root-certificate and intermediate-certificates flags
   192  	rootPEMBytes, err := os.ReadFile(filepath.Clean(rootPEM))
   193  	if err != nil {
   194  		return nil, nil, fmt.Errorf("error reading request from file: %w", err)
   195  	}
   196  
   197  	rootCerts, err := cryptoutils.UnmarshalCertificatesFromPEM(rootPEMBytes)
   198  	if err != nil {
   199  		return nil, nil, fmt.Errorf("failed to parse intermediate and root certs from PEM file: %w", err)
   200  	}
   201  
   202  	if len(rootCerts) == 0 {
   203  		return nil, nil, fmt.Errorf("expected at least one certificate to represent the root")
   204  	}
   205  
   206  	if intermediatePEM == "" {
   207  		return rootCerts, []*x509.Certificate{}, nil
   208  	}
   209  
   210  	// parse intermediate certificates
   211  	intermediatePEMBytes, err := os.ReadFile(filepath.Clean(intermediatePEM))
   212  	if err != nil {
   213  		return nil, nil, fmt.Errorf("error reading request from file: %w", err)
   214  	}
   215  
   216  	intermediateCerts, err := cryptoutils.UnmarshalCertificatesFromPEM(intermediatePEMBytes)
   217  	if err != nil {
   218  		return nil, nil, fmt.Errorf("failed to parse intermediate and root certs from PEM file: %w", err)
   219  	}
   220  
   221  	if len(intermediateCerts) == 0 {
   222  		return nil, nil, fmt.Errorf("expected at least one intermediate certificate")
   223  	}
   224  
   225  	return rootCerts, intermediateCerts, nil
   226  }
   227  
   228  func getOID() ([]int, error) {
   229  	oidFlagVal := viper.GetString("oid")
   230  	if oidFlagVal == "" {
   231  		return nil, nil
   232  	}
   233  
   234  	oidStrSlice := strings.Split(oidFlagVal, ".")
   235  	oid := make([]int, len(oidStrSlice))
   236  	for i, el := range oidStrSlice {
   237  		intVar, err := strconv.Atoi(el)
   238  		if err != nil {
   239  			return nil, err
   240  		}
   241  		oid[i] = intVar
   242  	}
   243  
   244  	return oid, nil
   245  }
   246  
   247  func parseTSACertificate(certPath string) (*x509.Certificate, error) {
   248  	pemBytes, err := os.ReadFile(filepath.Clean(certPath))
   249  	if err != nil {
   250  		return nil, fmt.Errorf("error reading TSA's certificate file: %w", err)
   251  	}
   252  
   253  	certs, err := cryptoutils.UnmarshalCertificatesFromPEM(pemBytes)
   254  	if err != nil {
   255  		return nil, fmt.Errorf("failed to parse TSA certificate during verification: %w", err)
   256  	}
   257  	if len(certs) != 1 {
   258  		return nil, fmt.Errorf("expected one certificate, received %d instead", len(certs))
   259  	}
   260  
   261  	return certs[0], nil
   262  }
   263  
   264  func init() {
   265  	initializePFlagMap()
   266  	addVerifyFlags(verifyCmd)
   267  	rootCmd.AddCommand(verifyCmd)
   268  }
   269  

View as plain text