Add support for file signatures in cli
This commit is contained in:
parent
345ceb01e4
commit
74d41194cc
5 changed files with 315 additions and 53 deletions
|
@ -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 != "-" {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
79
cmd/format_sig.go
Normal 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] }
|
|
@ -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)
|
||||
|
|
Loading…
Add table
Reference in a new issue