From 74d41194cc61044571a600775e9d68c0f8d358a6 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:04:48 +0200 Subject: [PATCH] Add support for file signatures in cli --- cmd/cmd-close.go | 6 +- cmd/cmd-open.go | 5 + cmd/cmd-verify.go | 271 ++++++++++++++++++++++++++++++++++++++-------- cmd/format_sig.go | 79 ++++++++++++++ cmd/main.go | 7 +- 5 files changed, 315 insertions(+), 53 deletions(-) create mode 100644 cmd/format_sig.go diff --git a/cmd/cmd-close.go b/cmd/cmd-close.go index bec65bb..62e7e2f 100644 --- a/cmd/cmd-close.go +++ b/cmd/cmd-close.go @@ -12,7 +12,7 @@ import ( func init() { rootCmd.AddCommand(closeCmd) - closeCmd.Flags().StringVarP(&closeFlagOutput, "output", "o", "", "specify output file (`-` for stdout") + closeCmd.Flags().StringVarP(&closeFlagOutput, "output", "o", "", "specify output file (`-` for stdout)") } var ( @@ -49,10 +49,10 @@ var ( filename := args[0] outputFilename := closeFlagOutput if outputFilename == "" { - if strings.HasSuffix(filename, ".letter") { + if strings.HasSuffix(filename, letterFileExtension) { return errors.New("cannot automatically derive output filename, please specify with --output") } - outputFilename = filename + ".letter" + outputFilename = filename + letterFileExtension } // check input file if filename != "-" { diff --git a/cmd/cmd-open.go b/cmd/cmd-open.go index b8a34e9..edf3ecf 100644 --- a/cmd/cmd-open.go +++ b/cmd/cmd-open.go @@ -93,6 +93,11 @@ var ( return err } + // Create default requirements if not set. + if requirements == nil { + requirements = jess.NewRequirements() + } + // decrypt (and verify) plainText, err := letter.Open(requirements, trustStore) if err != nil { diff --git a/cmd/cmd-verify.go b/cmd/cmd-verify.go index 6e36ad5..1efc832 100644 --- a/cmd/cmd-verify.go +++ b/cmd/cmd-verify.go @@ -3,85 +3,260 @@ package main import ( "errors" "fmt" + "io/fs" "io/ioutil" "os" + "path/filepath" + "strings" "github.com/spf13/cobra" "github.com/safing/jess" + "github.com/safing/jess/filesig" "github.com/safing/portbase/container" ) func init() { rootCmd.AddCommand(verifyCmd) + verifyCmd.Flags().StringToStringVarP(&metaDataFlag, "metadata", "m", nil, "specify file metadata to verify (.sig only)") } -var ( - verifyCmdHelp = "usage: jess verify <file>" +var verifyCmd = &cobra.Command{ + Use: "verify <files and directories>", + Short: "verify signed files and files in directories", + DisableFlagsInUseLine: true, + Args: cobra.MinimumNArgs(1), + PreRunE: requireTrustStore, + RunE: func(cmd *cobra.Command, args []string) (err error) { + var verificationFails, verificationWarnings int - verifyCmd = &cobra.Command{ - Use: "verify <file>", - Short: "verify file", - DisableFlagsInUseLine: true, - RunE: func(cmd *cobra.Command, args []string) (err error) { - // check args - if len(args) != 1 { - return errors.New(verifyCmdHelp) + // Check if we are only verifying a single file. + if len(args) == 1 { + matches, err := filepath.Glob(args[0]) + if err != nil { + return err } - // check filenames - filename := args[0] - // check input file - if filename != "-" { - fileInfo, err := os.Stat(filename) + switch len(matches) { + case 0: + return errors.New("file not found") + case 1: + // Check if the single match is a file. + fileInfo, err := os.Stat(matches[0]) if err != nil { return err } - if fileInfo.Size() > warnFileSize { - confirmed, err := confirm("Input file is really big (%s) and jess needs to load it fully to memory, continue?", true) - if err != nil { - return err - } - if !confirmed { - return nil - } + // Verify file if it is not a directory. + if !fileInfo.IsDir() { + return verify(matches[0], false) } } + } - // load file - var data []byte - if filename == "-" { - data, err = ioutil.ReadAll(os.Stdin) - } else { - data, err = ioutil.ReadFile(filename) - } + // Resolve globs. + files := make([]string, 0, len(args)) + for _, arg := range args { + matches, err := filepath.Glob(arg) if err != nil { return err } + files = append(files, matches...) + } - // parse file - letter, err := jess.LetterFromFileFormat(container.New(data)) + // Go through all files. + for _, file := range files { + fileInfo, err := os.Stat(file) if err != nil { - return err + verificationWarnings++ + fmt.Printf("[WARN] %s failed to read: %s\n", file, err) + continue } - // adjust requirements - if requirements == nil { - requirements = jess.NewRequirements(). - Remove(jess.Confidentiality). - Remove(jess.Integrity). - Remove(jess.RecipientAuthentication) + // Walk directories. + if fileInfo.IsDir() { + err := filepath.Walk(file, func(path string, info fs.FileInfo, err error) error { + // Log walking errors. + if err != nil { + verificationWarnings++ + fmt.Printf("[WARN] %s failed to read: %s\n", path, err) + return nil + } + + // Only verify if .sig or .letter. + if strings.HasSuffix(path, sigFileExtension) || + strings.HasSuffix(path, letterFileExtension) { + if err := verify(path, true); err != nil { + verificationFails++ + } + } + return nil + }) + if err != nil { + verificationWarnings++ + fmt.Printf("[WARN] %s failed to walk directory: %s\n", file, err) + } + continue } - // verify - err = letter.Verify(requirements, trustStore) - if err != nil { - return err + if err := verify(file, true); err != nil { + verificationFails++ } + } - // success - fmt.Println("ok") - return nil - }, + // End with error status if any verification failed. + if verificationFails > 0 { + return fmt.Errorf("%d verification failures", verificationFails) + } + if verificationWarnings > 0 { + return fmt.Errorf("%d warnings", verificationWarnings) + } + + return nil + }, +} + +var verifiedSigs = make(map[string]struct{}) + +func verify(filename string, bulkMode bool) error { + // Check if file was already verified. + if _, alreadyVerified := verifiedSigs[filename]; alreadyVerified { + return nil } -) + + var ( + signame string + signedBy []string + err error + ) + + // Get correct files and verify. + switch { + case filename == stdInOutFilename: + signedBy, err = verifyLetter(filename, bulkMode) + case strings.HasSuffix(filename, letterFileExtension): + signedBy, err = verifyLetter(filename, bulkMode) + case strings.HasSuffix(filename, sigFileExtension): + filename = strings.TrimSuffix(filename, sigFileExtension) + fallthrough + default: + signame = filename + sigFileExtension + signedBy, err = verifySig(filename, signame, bulkMode) + } + + // Remember the files already verified. + verifiedSigs[filename] = struct{}{} + if signame != "" { + verifiedSigs[signame] = struct{}{} + } + + // Output result in bulk mode. + if bulkMode { + if err == nil { + fmt.Printf("[ OK ] %s signed by %s\n", filename, strings.Join(signedBy, ", ")) + } else { + fmt.Printf("[FAIL] %s failed to verify: %s\n", filename, err) + } + } + + return err +} + +func verifyLetter(filename string, silent bool) (signedBy []string, err error) { + if len(metaDataFlag) > 0 { + return nil, errors.New("metadata flag only valid for verifying .sig files") + } + + if filename != "-" { + fileInfo, err := os.Stat(filename) + if err != nil { + return nil, err + } + if fileInfo.Size() > warnFileSize { + confirmed, err := confirm("Input file is really big (%s) and jess needs to load it fully to memory, continue?", true) + if err != nil { + return nil, err + } + if !confirmed { + return nil, nil + } + } + } + + // load file + var data []byte + if filename == "-" { + data, err = ioutil.ReadAll(os.Stdin) + } else { + data, err = ioutil.ReadFile(filename) + } + if err != nil { + return nil, err + } + + // parse file + letter, err := jess.LetterFromFileFormat(container.New(data)) + if err != nil { + return nil, err + } + + // Create default requirements if not set. + if requirements == nil { + requirements = jess.NewRequirements(). + Remove(jess.Confidentiality). + Remove(jess.RecipientAuthentication) + } + + // verify + err = letter.Verify(requirements, trustStore) + if err != nil { + return nil, err + } + + // get signers + signedBy = make([]string, 0, len(letter.Signatures)) + for _, seal := range letter.Signatures { + if signet, err := trustStore.GetSignet(seal.ID, true); err == nil { + signedBy = append(signedBy, fmt.Sprintf("%s (%s)", signet.Info.Name, seal.ID)) + } else { + signedBy = append(signedBy, seal.ID) + } + } + + // success + if !silent { + if err == nil { + fmt.Println("Verification: OK") + fmt.Printf("Signed By: %s\n", strings.Join(signedBy, ", ")) + } else { + fmt.Printf("Verification FAILED: %s\n\n", err) + } + } + + return signedBy, nil +} + +func verifySig(filename, signame string, silent bool) (signedBy []string, err error) { + fds, err := filesig.VerifyFile(filename, signame, metaDataFlag, trustStore) + if err != nil { + return nil, err + } + + if !silent { + fmt.Print(formatSignatures(filename, signame, fds)) + return nil, nil + } + + signedBy = make([]string, 0, len(fds)) + for _, fd := range fds { + if sig := fd.Signature(); sig != nil { + for _, seal := range sig.Signatures { + if signet, err := trustStore.GetSignet(seal.ID, true); err == nil { + signedBy = append(signedBy, fmt.Sprintf("%s (%s)", signet.Info.Name, seal.ID)) + } else { + signedBy = append(signedBy, seal.ID) + } + } + } + } + return signedBy, nil +} diff --git a/cmd/format_sig.go b/cmd/format_sig.go new file mode 100644 index 0000000..1970b2e --- /dev/null +++ b/cmd/format_sig.go @@ -0,0 +1,79 @@ +package main + +import ( + "encoding/hex" + "fmt" + "sort" + "strings" + + "github.com/safing/jess/filesig" +) + +func formatSignatures(filename, signame string, fds []*filesig.FileData) string { + b := &strings.Builder{} + + switch len(fds) { + case 0: + case 1: + formatSignature(b, fds[0]) + case 2: + for _, fd := range fds { + fmt.Fprintf(b, "%d Signatures:\n\n\n", len(fds)) + formatSignature(b, fd) + b.WriteString("\n\n") + } + } + + if filename != "" || signame != "" { + b.WriteString("\n") + fmt.Fprintf(b, "File: %s\n", filename) + fmt.Fprintf(b, "Sig: %s\n", signame) + } + + return b.String() +} + +func formatSignature(b *strings.Builder, fd *filesig.FileData) { + if fd.VerificationError() == nil { + b.WriteString("Verification: OK\n") + } else { + fmt.Fprintf(b, "Verification FAILED: %s\n", fd.VerificationError()) + } + + if letter := fd.Signature(); letter != nil { + b.WriteString("\n") + for _, sig := range letter.Signatures { + signet, err := trustStore.GetSignet(sig.ID, true) + if err == nil { + fmt.Fprintf(b, "Signed By: %s (%s)\n", signet.Info.Name, sig.ID) + } else { + fmt.Fprintf(b, "Signed By: %s\n", sig.ID) + } + } + } + + if fileHash := fd.FileHash(); fileHash != nil { + b.WriteString("\n") + fmt.Fprintf(b, "Hash Alg: %s\n", fileHash.Algorithm()) + fmt.Fprintf(b, "Hash Sum: %s\n", hex.EncodeToString(fileHash.Sum())) + } + + if len(fd.MetaData) > 0 { + b.WriteString("\nMetadata:\n") + + sortedMetaData := make([][]string, 0, len(fd.MetaData)) + for k, v := range fd.MetaData { + sortedMetaData = append(sortedMetaData, []string{k, v}) + } + sort.Sort(sortByMetaDataKey(sortedMetaData)) + for _, entry := range sortedMetaData { + fmt.Fprintf(b, " %s: %s\n", entry[0], entry[1]) + } + } +} + +type sortByMetaDataKey [][]string + +func (a sortByMetaDataKey) Len() int { return len(a) } +func (a sortByMetaDataKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a sortByMetaDataKey) Less(i, j int) bool { return a[i][0] < a[j][0] } diff --git a/cmd/main.go b/cmd/main.go index 2027948..93ad9d5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -14,6 +14,10 @@ import ( ) const ( + stdInOutFilename = "-" + letterFileExtension = ".letter" + sigFileExtension = ".sig" + warnFileSize = 12000000 // 120MB ) @@ -33,7 +37,7 @@ var ( defaultSymmetricKeySize = 0 trustStore truststores.ExtendedTrustStore - requirements = jess.NewRequirements() + requirements *jess.Requirements ) func main() { @@ -74,7 +78,6 @@ func initGlobalFlags(cmd *cobra.Command, args []string) (err error) { return err } } - // requirements if noSpec != "" { requirements, err = jess.ParseRequirementsFromNoSpec(noSpec)