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() {
|
func init() {
|
||||||
rootCmd.AddCommand(closeCmd)
|
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 (
|
var (
|
||||||
|
@ -49,10 +49,10 @@ var (
|
||||||
filename := args[0]
|
filename := args[0]
|
||||||
outputFilename := closeFlagOutput
|
outputFilename := closeFlagOutput
|
||||||
if outputFilename == "" {
|
if outputFilename == "" {
|
||||||
if strings.HasSuffix(filename, ".letter") {
|
if strings.HasSuffix(filename, letterFileExtension) {
|
||||||
return errors.New("cannot automatically derive output filename, please specify with --output")
|
return errors.New("cannot automatically derive output filename, please specify with --output")
|
||||||
}
|
}
|
||||||
outputFilename = filename + ".letter"
|
outputFilename = filename + letterFileExtension
|
||||||
}
|
}
|
||||||
// check input file
|
// check input file
|
||||||
if filename != "-" {
|
if filename != "-" {
|
||||||
|
|
|
@ -93,6 +93,11 @@ var (
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create default requirements if not set.
|
||||||
|
if requirements == nil {
|
||||||
|
requirements = jess.NewRequirements()
|
||||||
|
}
|
||||||
|
|
||||||
// decrypt (and verify)
|
// decrypt (and verify)
|
||||||
plainText, err := letter.Open(requirements, trustStore)
|
plainText, err := letter.Open(requirements, trustStore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -3,47 +3,181 @@ package main
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/safing/jess"
|
"github.com/safing/jess"
|
||||||
|
"github.com/safing/jess/filesig"
|
||||||
"github.com/safing/portbase/container"
|
"github.com/safing/portbase/container"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(verifyCmd)
|
rootCmd.AddCommand(verifyCmd)
|
||||||
|
verifyCmd.Flags().StringToStringVarP(&metaDataFlag, "metadata", "m", nil, "specify file metadata to verify (.sig only)")
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
// Check if we are only verifying a single file.
|
||||||
|
if len(args) == 1 {
|
||||||
|
matches, err := filepath.Glob(args[0])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
// Verify file if it is not a directory.
|
||||||
|
if !fileInfo.IsDir() {
|
||||||
|
return verify(matches[0], false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go through all files.
|
||||||
|
for _, file := range files {
|
||||||
|
fileInfo, err := os.Stat(file)
|
||||||
|
if err != nil {
|
||||||
|
verificationWarnings++
|
||||||
|
fmt.Printf("[WARN] %s failed to read: %s\n", file, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := verify(file, true); err != nil {
|
||||||
|
verificationFails++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (
|
var (
|
||||||
verifyCmdHelp = "usage: jess verify <file>"
|
signame string
|
||||||
|
signedBy []string
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
verifyCmd = &cobra.Command{
|
// Get correct files and verify.
|
||||||
Use: "verify <file>",
|
switch {
|
||||||
Short: "verify file",
|
case filename == stdInOutFilename:
|
||||||
DisableFlagsInUseLine: true,
|
signedBy, err = verifyLetter(filename, bulkMode)
|
||||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
case strings.HasSuffix(filename, letterFileExtension):
|
||||||
// check args
|
signedBy, err = verifyLetter(filename, bulkMode)
|
||||||
if len(args) != 1 {
|
case strings.HasSuffix(filename, sigFileExtension):
|
||||||
return errors.New(verifyCmdHelp)
|
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")
|
||||||
}
|
}
|
||||||
|
|
||||||
// check filenames
|
|
||||||
filename := args[0]
|
|
||||||
// check input file
|
|
||||||
if filename != "-" {
|
if filename != "-" {
|
||||||
fileInfo, err := os.Stat(filename)
|
fileInfo, err := os.Stat(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if fileInfo.Size() > warnFileSize {
|
if fileInfo.Size() > warnFileSize {
|
||||||
confirmed, err := confirm("Input file is really big (%s) and jess needs to load it fully to memory, continue?", true)
|
confirmed, err := confirm("Input file is really big (%s) and jess needs to load it fully to memory, continue?", true)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if !confirmed {
|
if !confirmed {
|
||||||
return nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -56,32 +190,73 @@ var (
|
||||||
data, err = ioutil.ReadFile(filename)
|
data, err = ioutil.ReadFile(filename)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse file
|
// parse file
|
||||||
letter, err := jess.LetterFromFileFormat(container.New(data))
|
letter, err := jess.LetterFromFileFormat(container.New(data))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// adjust requirements
|
// Create default requirements if not set.
|
||||||
if requirements == nil {
|
if requirements == nil {
|
||||||
requirements = jess.NewRequirements().
|
requirements = jess.NewRequirements().
|
||||||
Remove(jess.Confidentiality).
|
Remove(jess.Confidentiality).
|
||||||
Remove(jess.Integrity).
|
|
||||||
Remove(jess.RecipientAuthentication)
|
Remove(jess.RecipientAuthentication)
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify
|
// verify
|
||||||
err = letter.Verify(requirements, trustStore)
|
err = letter.Verify(requirements, trustStore)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
// success
|
||||||
fmt.Println("ok")
|
if !silent {
|
||||||
return nil
|
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 (
|
const (
|
||||||
|
stdInOutFilename = "-"
|
||||||
|
letterFileExtension = ".letter"
|
||||||
|
sigFileExtension = ".sig"
|
||||||
|
|
||||||
warnFileSize = 12000000 // 120MB
|
warnFileSize = 12000000 // 120MB
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -33,7 +37,7 @@ var (
|
||||||
defaultSymmetricKeySize = 0
|
defaultSymmetricKeySize = 0
|
||||||
|
|
||||||
trustStore truststores.ExtendedTrustStore
|
trustStore truststores.ExtendedTrustStore
|
||||||
requirements = jess.NewRequirements()
|
requirements *jess.Requirements
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
@ -74,7 +78,6 @@ func initGlobalFlags(cmd *cobra.Command, args []string) (err error) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// requirements
|
// requirements
|
||||||
if noSpec != "" {
|
if noSpec != "" {
|
||||||
requirements, err = jess.ParseRequirementsFromNoSpec(noSpec)
|
requirements, err = jess.ParseRequirementsFromNoSpec(noSpec)
|
||||||
|
|
Loading…
Add table
Reference in a new issue