git.sr.ht/~pingoo/stdx@v0.0.0-20240218134121-094174641f6e/tools/zign/main.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"encoding/hex"
     6  	"encoding/json"
     7  	"errors"
     8  	"fmt"
     9  	"os"
    10  	"strings"
    11  
    12  	"git.sr.ht/~pingoo/stdx/cobra"
    13  	"git.sr.ht/~pingoo/stdx/crypto"
    14  	"git.sr.ht/~pingoo/stdx/filex"
    15  	"git.sr.ht/~pingoo/stdx/zign"
    16  	"golang.org/x/term"
    17  )
    18  
    19  const (
    20  	defaultPrivateKeyFile = "zign.private"
    21  	defaultPulicKeyFile   = "zign.public"
    22  )
    23  
    24  var (
    25  	signManifestOutpout  string
    26  	signCmdPasswordStdin bool
    27  
    28  	version = fmt.Sprintf("%d.0.0", zign.Version1)
    29  )
    30  
    31  func init() {
    32  	rootCmd.AddCommand(initCmd)
    33  
    34  	signCmd.Flags().StringVarP(&signManifestOutpout, "output", "o", zign.DefaultManifestFilename, "Output file for signature (default: zign.json)")
    35  	signCmd.Flags().BoolVar(&signCmdPasswordStdin, "password-stdin", false, "Read password from stdin")
    36  	rootCmd.AddCommand(signCmd)
    37  
    38  	rootCmd.AddCommand(verifyCmd)
    39  }
    40  
    41  func main() {
    42  	err := rootCmd.Execute()
    43  	if err != nil {
    44  		fmt.Fprintln(os.Stdout, err.Error())
    45  		os.Exit(1)
    46  	}
    47  }
    48  
    49  var rootCmd = &cobra.Command{
    50  	Use:           "zign",
    51  	Short:         "Sign and verify files",
    52  	Version:       version,
    53  	SilenceUsage:  true,
    54  	SilenceErrors: true,
    55  	RunE: func(cmd *cobra.Command, args []string) (err error) {
    56  		return cmd.Help()
    57  	},
    58  }
    59  
    60  var initCmd = &cobra.Command{
    61  	Use:   "init",
    62  	Short: "Initialize a keypair to sign files (zign.private, zign.public)",
    63  	RunE: func(cmd *cobra.Command, args []string) (err error) {
    64  		fmt.Print("password: ")
    65  		password, err := term.ReadPassword(int(os.Stdin.Fd()))
    66  		if err != nil {
    67  			err = fmt.Errorf("zign: error reading password: %w", err)
    68  			return
    69  		}
    70  		defer crypto.Zeroize(password)
    71  
    72  		fmt.Print("\nverify password: ")
    73  		passwordVerification, err := term.ReadPassword(int(os.Stdin.Fd()))
    74  		if err != nil {
    75  			err = fmt.Errorf("zign: error reading password: %w", err)
    76  			return
    77  		}
    78  		defer crypto.Zeroize(passwordVerification)
    79  
    80  		if crypto.ConstantTimeCompare(password, passwordVerification) == false {
    81  			err = errors.New("zign: passwords don't match")
    82  			return
    83  		}
    84  
    85  		fmt.Println("")
    86  
    87  		privateKey, publicKey, err := zign.Init(password)
    88  		if err != nil {
    89  			return
    90  		}
    91  
    92  		privateKey += "\n"
    93  
    94  		err = os.WriteFile(defaultPrivateKeyFile, []byte(privateKey), 0600)
    95  		if err != nil {
    96  			err = fmt.Errorf("zign: writing private key file: %w", err)
    97  			return
    98  		}
    99  
   100  		publicKey += "\n"
   101  
   102  		err = os.WriteFile(defaultPulicKeyFile, []byte(publicKey), 0600)
   103  		if err != nil {
   104  			err = fmt.Errorf("zign: writing public key file: %w", err)
   105  			return
   106  		}
   107  
   108  		return
   109  	},
   110  }
   111  
   112  var signCmd = &cobra.Command{
   113  	Use:   "sign [-o project_version_zign.json] zign.private file1 file2 file3...",
   114  	Short: "Sign files using the given private key",
   115  	Args:  cobra.MinimumNArgs(2),
   116  	RunE: func(cmd *cobra.Command, args []string) (err error) {
   117  		var password []byte
   118  
   119  		if len(args) < 2 {
   120  			return cmd.Help()
   121  		}
   122  
   123  		if signCmdPasswordStdin {
   124  			stdinReader := bufio.NewReader(os.Stdin)
   125  			password, _, err = stdinReader.ReadLine()
   126  			if err != nil {
   127  				err = fmt.Errorf("zign: error reading password from stdin: %w", err)
   128  				return
   129  			}
   130  		} else {
   131  			fmt.Print("password: ")
   132  			password, err = term.ReadPassword(int(os.Stdin.Fd()))
   133  			if err != nil {
   134  				err = fmt.Errorf("zign: error reading password: %w", err)
   135  				return
   136  			}
   137  
   138  			fmt.Println("")
   139  		}
   140  		defer crypto.Zeroize(password)
   141  
   142  		encodedPrivateKeyBytes, err := os.ReadFile(defaultPrivateKeyFile)
   143  		if err != nil {
   144  			err = fmt.Errorf("zign: reading private key file: %w", err)
   145  			return
   146  		}
   147  
   148  		encodedPrivateKey := strings.TrimSpace(string(encodedPrivateKeyBytes))
   149  
   150  		files := args[1:]
   151  		signInput := make([]zign.SignInput, len(files))
   152  
   153  		for index, file := range files {
   154  			fileExists := false
   155  			var fileHandle *os.File
   156  
   157  			fileExists, err = filex.Exists(file)
   158  			if err != nil {
   159  				err = fmt.Errorf("checking if file exists (%s): %w", file, err)
   160  				return
   161  			}
   162  
   163  			if !fileExists {
   164  				err = fmt.Errorf("file does not exist: %s", file)
   165  				return
   166  			}
   167  
   168  			// we don't need to close the files as the program is short lived...
   169  			fileHandle, err = os.Open(file)
   170  			if err != nil {
   171  				err = fmt.Errorf("opening file (%s): %w", file, err)
   172  				return
   173  			}
   174  			fileSignInput := zign.SignInput{
   175  				Reader: fileHandle,
   176  			}
   177  			signInput[index] = fileSignInput
   178  		}
   179  
   180  		signOutput, err := zign.SignMany(encodedPrivateKey, string(password), signInput)
   181  		if err != nil {
   182  			return
   183  		}
   184  
   185  		manifest := zign.GenerateManifest(signOutput)
   186  		manifestJson, err := manifest.ToJson()
   187  		if err != nil {
   188  			return
   189  		}
   190  
   191  		err = os.WriteFile(signManifestOutpout, manifestJson, 0644)
   192  		if err != nil {
   193  			err = fmt.Errorf("zign: writing manifest (%s): %w", signManifestOutpout, err)
   194  			return
   195  		}
   196  
   197  		return
   198  	},
   199  }
   200  
   201  var verifyCmd = &cobra.Command{
   202  	Use:   "verify [base64_encoded_public_key] project_version_zign.json",
   203  	Short: "Verify files using the given public key",
   204  	Args:  cobra.ExactArgs(2),
   205  	RunE: func(cmd *cobra.Command, args []string) (err error) {
   206  		var manifest zign.Manifest
   207  		if len(args) != 2 {
   208  			return cmd.Help()
   209  		}
   210  
   211  		manifestJson, err := os.ReadFile(args[1])
   212  		if err != nil {
   213  			return
   214  		}
   215  
   216  		err = json.Unmarshal(manifestJson, &manifest)
   217  		if err != nil {
   218  			err = fmt.Errorf("parsing manifest: %w", err)
   219  			return
   220  		}
   221  
   222  		if manifest.Version != zign.Version1 {
   223  			err = fmt.Errorf("zign: manifest version superior to %s are not supported", zign.Version1)
   224  			return
   225  		}
   226  
   227  		verifyInput := make([]zign.VerifyInput, 0, len(manifest.Files))
   228  		verifiedFiles := make([]string, 0, len(manifest.Files))
   229  
   230  		for _, file := range manifest.Files {
   231  			fileExists := false
   232  			var fileHandle *os.File
   233  			var hash []byte
   234  
   235  			fileExists, err = filex.Exists(file.Filename)
   236  			if err != nil {
   237  				err = fmt.Errorf("checking if file exists (%s): %w", file.Filename, err)
   238  				return
   239  			}
   240  
   241  			if !fileExists {
   242  				continue
   243  			}
   244  
   245  			// we don't need to close the files as the program is short lived...
   246  			fileHandle, err = os.Open(file.Filename)
   247  			if err != nil {
   248  				err = fmt.Errorf("opening file (%s): %w", file.Filename, err)
   249  				return
   250  			}
   251  
   252  			hash, err = hex.DecodeString(file.HashBlake3)
   253  			if err != nil {
   254  				err = fmt.Errorf("decoding blake3 hash for file %s: %w", file.Filename, err)
   255  				return
   256  			}
   257  
   258  			fileVerifyInput := zign.VerifyInput{
   259  				Reader:     fileHandle,
   260  				HashBlake3: hash,
   261  				Signature:  file.Signature,
   262  			}
   263  			verifyInput = append(verifyInput, fileVerifyInput)
   264  			verifiedFiles = append(verifiedFiles, file.Filename)
   265  		}
   266  
   267  		base64EncodedPublicKey := strings.TrimSpace(args[0])
   268  		err = zign.VerifyMany(base64EncodedPublicKey, verifyInput)
   269  		if err != nil {
   270  			return
   271  		}
   272  
   273  		if len(verifiedFiles) == 0 {
   274  			fmt.Println("No file to verify")
   275  			return
   276  		}
   277  
   278  		for _, file := range verifiedFiles {
   279  			fmt.Println("✓", file)
   280  		}
   281  
   282  		return
   283  	},
   284  }