Add support for file signatures in cli

This commit is contained in:
Daniel 2022-07-11 17:04:48 +02:00
parent 345ceb01e4
commit 74d41194cc
5 changed files with 315 additions and 53 deletions

View file

@ -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 != "-" {

View file

@ -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 {

View file

@ -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
}

79
cmd/format_sig.go Normal file
View file

@ -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] }

View file

@ -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)