Merge pull request from safing/feature/file-sigs

Add file signature support
This commit is contained in:
Daniel Hovie 2022-09-29 10:05:06 +02:00 committed by GitHub
commit a63e42986a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 2225 additions and 137 deletions

View file

@ -20,14 +20,14 @@ jobs:
- uses: actions/setup-go@v3 - uses: actions/setup-go@v3
with: with:
go-version: '^1.18' go-version: '^1.19'
- name: Run golangci-lint - name: Run golangci-lint
uses: golangci/golangci-lint-action@v3 uses: golangci/golangci-lint-action@v3
with: with:
version: v1.45.1 version: v1.49.0
only-new-issues: true only-new-issues: true
args: -c ./.golangci.yml args: -c ./.golangci.yml --timeout 15m
- name: Get dependencies - name: Get dependencies
run: go mod download run: go mod download
@ -45,7 +45,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
go-version: '^1.18' go-version: '^1.19'
- name: Get dependencies - name: Get dependencies
run: go mod download run: go mod download

3
.gitignore vendored
View file

@ -1,8 +1,7 @@
cpu.out cpu.out
vendor vendor
cmd/cmd*
cmd/jess* cmd/jess*
dist dist
# Custom dev deops # Custom dev deps
go.mod.* go.mod.*

View file

@ -7,8 +7,8 @@ linters:
- containedctx - containedctx
- contextcheck - contextcheck
- cyclop - cyclop
- errchkjson # Triggers stack overflow on github.com/safing/jess
- exhaustivestruct - exhaustivestruct
- exhaustruct
- forbidigo - forbidigo
- funlen - funlen
- gochecknoglobals - gochecknoglobals
@ -18,6 +18,7 @@ linters:
- goerr113 - goerr113
- gomnd - gomnd
- ifshort - ifshort
- interfacebloat
- interfacer - interfacer
- ireturn - ireturn
- lll - lll
@ -25,6 +26,9 @@ linters:
- nilnil - nilnil
- nlreturn - nlreturn
- noctx - noctx
- nolintlint
- nonamedreturns
- nosnakecase
- revive - revive
- tagliatelle - tagliatelle
- testpackage - testpackage

View file

@ -50,6 +50,12 @@ echo "Please notice, that this build script includes metadata into the build."
echo "This information is useful for debugging and license compliance." echo "This information is useful for debugging and license compliance."
echo "Run the compiled binary with the version command to see the information included." echo "Run the compiled binary with the version command to see the information included."
# build output name
BIN_NAME="jess"
if [[ "$GOOS" == "windows" ]]; then
BIN_NAME="${BIN_NAME}.exe"
fi
# build # build
BUILD_PATH="github.com/safing/jess/vendor/github.com/safing/portbase/info" BUILD_PATH="github.com/safing/portbase/info"
go build -ldflags "-X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" $@ go build -o "${BIN_NAME}" -ldflags "-X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" "$@"

View file

@ -30,7 +30,8 @@ func newEnvelope(name string) (*jess.Envelope, error) {
"Encrypt with key", "Encrypt with key",
"Encrypt for someone and sign", "Encrypt for someone and sign",
"Encrypt for someone but don't sign", "Encrypt for someone but don't sign",
"Sign a file", "Sign a file (wrapped)",
"Sign a file (separate sig)",
}, },
} }
err := survey.AskOne(prompt, &preset, nil) err := survey.AskOne(prompt, &preset, nil)
@ -54,9 +55,12 @@ func newEnvelope(name string) (*jess.Envelope, error) {
case "Encrypt for someone but don't sign": case "Encrypt for someone but don't sign":
envelope.SuiteID = jess.SuiteRcptOnly envelope.SuiteID = jess.SuiteRcptOnly
err = selectSignets(envelope, "recipient") err = selectSignets(envelope, "recipient")
case "Sign a file": case "Sign a file (wrapped)":
envelope.SuiteID = jess.SuiteSign envelope.SuiteID = jess.SuiteSign
err = selectSignets(envelope, "sender") err = selectSignets(envelope, "sender")
case "Sign a file (separate sig)":
envelope.SuiteID = jess.SuiteSignFile
err = selectSignets(envelope, "sender")
} }
if err != nil { if err != nil {
return nil, err return nil, err
@ -93,6 +97,7 @@ func editEnvelope(envelope *jess.Envelope) error {
{"Recipients", formatSignetNames(envelope.Recipients)}, {"Recipients", formatSignetNames(envelope.Recipients)},
{"Senders", formatSignetNames(envelope.Senders)}, {"Senders", formatSignetNames(envelope.Senders)},
{""}, {""},
{"Export", "export to text format"},
{"Abort", "discard changes and return"}, {"Abort", "discard changes and return"},
{"Delete", "delete and return"}, {"Delete", "delete and return"},
}), }),
@ -105,8 +110,28 @@ func editEnvelope(envelope *jess.Envelope) error {
switch { switch {
case strings.HasPrefix(submenu, "Done"): case strings.HasPrefix(submenu, "Done"):
// save // Check if the envelope is valid.
if envelope.SecurityLevel == 0 {
fmt.Println("Envelope is invalid, please fix before saving.")
continue
}
// Remove and keys and save.
envelope.CleanSignets()
return trustStore.StoreEnvelope(envelope) return trustStore.StoreEnvelope(envelope)
case strings.HasPrefix(submenu, "Export"):
// Check if the envelope is valid.
if envelope.SecurityLevel == 0 {
fmt.Println("Envelope is invalid, please fix before exporting.")
continue
}
// Remove keys and export.
envelope.CleanSignets()
text, err := envelope.Export(false)
if err != nil {
return fmt.Errorf("failed to export: %w", err)
}
fmt.Println("Exported envelope:")
fmt.Println(text)
case strings.HasPrefix(submenu, "Abort"): case strings.HasPrefix(submenu, "Abort"):
return nil return nil
case strings.HasPrefix(submenu, "Delete"): case strings.HasPrefix(submenu, "Delete"):

View file

@ -12,7 +12,7 @@ import (
) )
//nolint:gocognit //nolint:gocognit
func newSignet(name, scheme string) (*jess.Signet, error) { func newSignet(name, scheme string, saveToTrustStore bool) (*jess.Signet, error) {
// get name // get name
name = strings.TrimSpace(name) name = strings.TrimSpace(name)
if name == "" { if name == "" {
@ -110,28 +110,30 @@ func newSignet(name, scheme string) (*jess.Signet, error) {
Created: time.Now(), Created: time.Now(),
} }
// write signet if saveToTrustStore {
err = trustStore.StoreSignet(signet) // write signet
if err != nil { err = trustStore.StoreSignet(signet)
return nil, err if err != nil {
} return nil, err
}
// export as recipient // export as recipient
switch scheme { switch scheme {
case jess.SignetSchemePassword, jess.SignetSchemeKey: case jess.SignetSchemePassword, jess.SignetSchemeKey:
// is secret, no recipient // is secret, no recipient
default: default:
rcpt, err := signet.AsRecipient() rcpt, err := signet.AsRecipient()
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = rcpt.StoreKey() err = rcpt.StoreKey()
if err != nil { if err != nil {
return nil, err return nil, err
} }
err = trustStore.StoreSignet(rcpt) err = trustStore.StoreSignet(rcpt)
if err != nil { if err != nil {
return nil, err return nil, err
}
} }
} }

View file

@ -3,7 +3,7 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"os" "os"
"strings" "strings"
@ -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 != "-" {
@ -89,9 +89,9 @@ var (
// load file // load file
var data []byte var data []byte
if filename == "-" { if filename == "-" {
data, err = ioutil.ReadAll(os.Stdin) data, err = io.ReadAll(os.Stdin)
} else { } else {
data, err = ioutil.ReadFile(filename) data, err = os.ReadFile(filename)
} }
if err != nil { if err != nil {
return err return err

View file

@ -1,6 +1,8 @@
package main package main
import ( import (
"fmt"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@ -8,11 +10,13 @@ func init() {
rootCmd.AddCommand(generateCmd) rootCmd.AddCommand(generateCmd)
generateCmd.Flags().StringVarP(&generateFlagName, "name", "l", "", "specify signet name/label") generateCmd.Flags().StringVarP(&generateFlagName, "name", "l", "", "specify signet name/label")
generateCmd.Flags().StringVarP(&generateFlagScheme, "scheme", "t", "", "specify signet scheme/tool") generateCmd.Flags().StringVarP(&generateFlagScheme, "scheme", "t", "", "specify signet scheme/tool")
generateCmd.Flags().BoolVarP(&generateFlagTextOnly, "textonly", "", false, "do not save to trust store and only output directly as text")
} }
var ( var (
generateFlagName string generateFlagName string
generateFlagScheme string generateFlagScheme string
generateFlagTextOnly bool
generateCmd = &cobra.Command{ generateCmd = &cobra.Command{
Use: "generate", Use: "generate",
@ -21,8 +25,43 @@ var (
Args: cobra.NoArgs, Args: cobra.NoArgs,
PreRunE: requireTrustStore, PreRunE: requireTrustStore,
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
_, err := newSignet(generateFlagName, generateFlagScheme) // Generate new signet
return err signet, err := newSignet(generateFlagName, generateFlagScheme, !generateFlagTextOnly)
if err != nil {
return err
}
// Output as text if not saved to trust store.
if generateFlagTextOnly {
// Make text backup.
backup, err := signet.Backup(false)
if err != nil {
return err
}
// Convert to recipient and serialize key.
rcpt, err := signet.AsRecipient()
if err != nil {
return err
}
err = rcpt.StoreKey()
if err != nil {
return err
}
// Make text export.
export, err := rcpt.Export(false)
if err != nil {
return err
}
// Write output.
fmt.Printf("Generated %s key with ID %s and name %q\n", signet.Scheme, signet.ID, signet.Info.Name)
fmt.Printf("Backup (private key): %s\n", backup)
fmt.Printf("Export (public key): %s\n", export)
}
return nil
}, },
} }
) )

170
cmd/cmd-import-export.go Normal file
View file

@ -0,0 +1,170 @@
package main
import (
"errors"
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/safing/jess"
)
func init() {
rootCmd.AddCommand(exportCmd)
rootCmd.AddCommand(backupCmd)
rootCmd.AddCommand(importCmd)
}
var (
exportCmdHelp = "usage: export <id>"
exportCmd = &cobra.Command{
Use: "export <id>",
Short: "export a signet or envelope",
Long: "export a signet (as a recipient - the public key only) or an envelope (configuration)",
RunE: handleExport,
}
backupCmdHelp = "usage: backup <id"
backupCmd = &cobra.Command{
Use: "backup <id>",
Short: "backup a signet",
Long: "backup a signet (the private key - do not share!)",
RunE: handleBackup,
}
importCmdHelp = "usage: import <text>"
importCmd = &cobra.Command{
Use: "import <text>",
Short: "import a signet or an enveleope",
Long: "import a signet (any kind) or an enveleope",
RunE: handleImport,
}
)
func handleExport(cmd *cobra.Command, args []string) error {
// Check args.
if len(args) != 1 {
return errors.New(exportCmdHelp)
}
id := args[0]
// Get Recipient.
recipient, err := trustStore.GetSignet(id, true)
if err == nil {
text, err := recipient.Export(false)
if err != nil {
return fmt.Errorf("failed to export recipient %s: %w", id, err)
}
fmt.Println(text)
return nil
}
// Check if there is a signet instead.
signet, err := trustStore.GetSignet(id, false)
if err == nil {
recipient, err := signet.AsRecipient()
if err != nil {
return fmt.Errorf("failed convert signet %s to recipient for export: %w", id, err)
}
text, err := recipient.Export(false)
if err != nil {
return fmt.Errorf("failed to export recipient %s: %w", id, err)
}
fmt.Println(text)
return nil
}
// Check for an envelope.
env, err := trustStore.GetEnvelope(id)
if err == nil {
text, err := env.Export(false)
if err != nil {
return fmt.Errorf("failed to export envelope %s: %w", id, err)
}
fmt.Println(text)
return nil
}
return errors.New("no recipient or envelope found with the given ID")
}
func handleBackup(cmd *cobra.Command, args []string) error {
// Check args.
if len(args) != 1 {
return errors.New(backupCmdHelp)
}
id := args[0]
// Check if there is a signet instead.
signet, err := trustStore.GetSignet(id, false)
if err != nil {
text, err := signet.Backup(false)
if err != nil {
return fmt.Errorf("failed to backup signet %s: %w", id, err)
}
fmt.Println(text)
return nil
}
return errors.New("no signet found with the given ID")
}
func handleImport(cmd *cobra.Command, args []string) error {
// Check args.
if len(args) != 1 {
return errors.New(importCmdHelp)
}
text := args[0]
// First, check if it's an envelope.
if strings.HasPrefix(text, jess.ExportEnvelopePrefix) {
env, err := jess.EnvelopeFromTextFormat(text)
if err != nil {
return fmt.Errorf("failed to parse envelope: %w", err)
}
err = trustStore.StoreEnvelope(env)
if err != nil {
return fmt.Errorf("failed to import envelope into trust store: %w", err)
}
fmt.Printf("imported envelope %q intro trust store\n", env.Name)
return nil
}
// Then handle all signet types together.
var (
signetType string
parseFunc func(textFormat string) (*jess.Signet, error)
)
switch {
case strings.HasPrefix(text, jess.ExportSenderPrefix):
signetType = jess.ExportSenderKeyword
parseFunc = jess.SenderFromTextFormat
case strings.HasPrefix(text, jess.ExportRecipientPrefix):
signetType = jess.ExportRecipientKeyword
parseFunc = jess.RecipientFromTextFormat
case strings.HasPrefix(text, jess.ExportKeyPrefix):
signetType = jess.ExportKeyKeyword
parseFunc = jess.KeyFromTextFormat
default:
return fmt.Errorf(
"invalid format or unknown type, expected one of %s, %s, %s, %s",
jess.ExportKeyKeyword,
jess.ExportSenderKeyword,
jess.ExportRecipientKeyword,
jess.ExportEnvelopeKeyword,
)
}
// Parse and import
signet, err := parseFunc(text)
if err != nil {
return fmt.Errorf("failed to parse %s: %w", signetType, err)
}
err = trustStore.StoreSignet(signet)
if err != nil {
return fmt.Errorf("failed to import %s into trust store: %w", signetType, err)
}
fmt.Printf("imported %s %s intro trust store\n", signetType, signet.ID)
return nil
}

View file

@ -4,7 +4,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"os" "os"
"strings" "strings"
@ -79,9 +78,9 @@ var (
// load file // load file
var data []byte var data []byte
if filename == "-" { if filename == "-" {
data, err = ioutil.ReadAll(os.Stdin) data, err = io.ReadAll(os.Stdin)
} else { } else {
data, err = ioutil.ReadFile(filename) data, err = os.ReadFile(filename)
} }
if err != nil { if err != nil {
return err return err
@ -93,6 +92,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 {

62
cmd/cmd-sign.go Normal file
View file

@ -0,0 +1,62 @@
package main
import (
"errors"
"fmt"
"strings"
"github.com/spf13/cobra"
"github.com/safing/jess/filesig"
)
func init() {
rootCmd.AddCommand(signCmd)
signCmd.Flags().StringVarP(&closeFlagOutput, "output", "o", "", "specify output file (`-` for stdout")
signCmd.Flags().StringToStringVarP(&metaDataFlag, "metadata", "m", nil, "specify file metadata to sign")
}
var (
metaDataFlag map[string]string
signCmdHelp = "usage: jess sign <file> with <envelope name>"
signCmd = &cobra.Command{
Use: "sign <file> with <envelope name>",
Short: "sign file",
Long: "sign file with the given envelope. Use `-` to use stdin",
DisableFlagsInUseLine: true,
PreRunE: requireTrustStore,
RunE: func(cmd *cobra.Command, args []string) error {
registerPasswordCallbacks()
// check args
if len(args) != 3 || args[1] != "with" {
return errors.New(signCmdHelp)
}
// get envelope
envelope, err := trustStore.GetEnvelope(args[2])
if err != nil {
return err
}
// check filenames
filename := args[0]
outputFilename := closeFlagOutput
if outputFilename == "" {
if strings.HasSuffix(filename, filesig.Extension) {
return errors.New("cannot automatically derive output filename, please specify with --output")
}
outputFilename = filename + filesig.Extension
}
fd, err := filesig.SignFile(filename, outputFilename, metaDataFlag, envelope, trustStore)
if err != nil {
return err
}
fmt.Print(formatSignatures(filename, outputFilename, []*filesig.FileData{fd}))
return nil
},
}
)

View file

@ -3,85 +3,260 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/ioutil" "io"
"io/fs"
"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 ( var verifyCmd = &cobra.Command{
verifyCmdHelp = "usage: jess verify <file>" 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{ // Check if we are only verifying a single file.
Use: "verify <file>", if len(args) == 1 {
Short: "verify file", matches, err := filepath.Glob(args[0])
DisableFlagsInUseLine: true, if err != nil {
RunE: func(cmd *cobra.Command, args []string) (err error) { return err
// check args
if len(args) != 1 {
return errors.New(verifyCmdHelp)
} }
// check filenames switch len(matches) {
filename := args[0] case 0:
// check input file return errors.New("file not found")
if filename != "-" { case 1:
fileInfo, err := os.Stat(filename) // Check if the single match is a file.
fileInfo, err := os.Stat(matches[0])
if err != nil { if err != nil {
return err return err
} }
if fileInfo.Size() > warnFileSize { // Verify file if it is not a directory.
confirmed, err := confirm("Input file is really big (%s) and jess needs to load it fully to memory, continue?", true) if !fileInfo.IsDir() {
if err != nil { return verify(matches[0], false)
return err
}
if !confirmed {
return nil
}
} }
} }
}
// load file // Resolve globs.
var data []byte files := make([]string, 0, len(args))
if filename == "-" { for _, arg := range args {
data, err = ioutil.ReadAll(os.Stdin) matches, err := filepath.Glob(arg)
} else {
data, err = ioutil.ReadFile(filename)
}
if err != nil { if err != nil {
return err return err
} }
files = append(files, matches...)
}
// parse file // Go through all files.
letter, err := jess.LetterFromFileFormat(container.New(data)) for _, file := range files {
fileInfo, err := os.Stat(file)
if err != nil { if err != nil {
return err verificationWarnings++
fmt.Printf("[WARN] %s failed to read: %s\n", file, err)
continue
} }
// adjust requirements // Walk directories.
if requirements == nil { if fileInfo.IsDir() {
requirements = jess.NewRequirements(). err := filepath.Walk(file, func(path string, info fs.FileInfo, err error) error {
Remove(jess.Confidentiality). // Log walking errors.
Remove(jess.Integrity). if err != nil {
Remove(jess.RecipientAuthentication) verificationWarnings++
fmt.Printf("[WARN] %s failed to read: %s\n", path, err)
return nil
}
// Only verify if .sig or .letter.
if strings.HasSuffix(path, filesig.Extension) ||
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 if err := verify(file, true); err != nil {
err = letter.Verify(requirements, trustStore) verificationFails++
if err != nil {
return err
} }
}
// success // End with error status if any verification failed.
fmt.Println("ok") if verificationFails > 0 {
return nil 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, filesig.Extension):
filename = strings.TrimSuffix(filename, filesig.Extension)
fallthrough
default:
signame = filename + filesig.Extension
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 = io.ReadAll(os.Stdin)
} else {
data, err = os.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,9 @@ import (
) )
const ( const (
stdInOutFilename = "-"
letterFileExtension = ".letter"
warnFileSize = 12000000 // 120MB warnFileSize = 12000000 // 120MB
) )
@ -28,12 +31,13 @@ var (
} }
trustStoreDir string trustStoreDir string
trustStoreKeyring string
noSpec string noSpec string
minimumSecurityLevel = 0 minimumSecurityLevel = 0
defaultSymmetricKeySize = 0 defaultSymmetricKeySize = 0
trustStore truststores.ExtendedTrustStore trustStore truststores.ExtendedTrustStore
requirements = jess.NewRequirements() requirements *jess.Requirements
) )
func main() { func main() {
@ -46,7 +50,10 @@ func main() {
} }
rootCmd.PersistentFlags().StringVarP(&trustStoreDir, "tsdir", "d", "", rootCmd.PersistentFlags().StringVarP(&trustStoreDir, "tsdir", "d", "",
"specify a truststore directory (default loaded from JESS_TSDIR env variable)", "specify a truststore directory (default loaded from JESS_TS_DIR env variable)",
)
rootCmd.PersistentFlags().StringVarP(&trustStoreKeyring, "tskeyring", "r", "",
"specify a truststore keyring namespace (default loaded from JESS_TS_KEYRING env variable) - lower priority than tsdir",
) )
rootCmd.PersistentFlags().StringVarP(&noSpec, "no", "n", "", rootCmd.PersistentFlags().StringVarP(&noSpec, "no", "n", "",
"remove requirements using the abbreviations C, I, R, S", "remove requirements using the abbreviations C, I, R, S",
@ -63,18 +70,36 @@ func main() {
} }
func initGlobalFlags(cmd *cobra.Command, args []string) (err error) { func initGlobalFlags(cmd *cobra.Command, args []string) (err error) {
// trust store // trust store directory
if trustStoreDir == "" { if trustStoreDir == "" {
trustStoreDir, _ = os.LookupEnv("JESS_TSDIR") trustStoreDir, _ = os.LookupEnv("JESS_TS_DIR")
if trustStoreDir == "" {
trustStoreDir, _ = os.LookupEnv("JESS_TSDIR")
}
} }
if trustStoreDir != "" { if trustStoreDir != "" {
var err error
trustStore, err = truststores.NewDirTrustStore(trustStoreDir) trustStore, err = truststores.NewDirTrustStore(trustStoreDir)
if err != nil { if err != nil {
return err return err
} }
} }
// trust store keyring
if trustStore == nil {
if trustStoreKeyring == "" {
trustStoreKeyring, _ = os.LookupEnv("JESS_TS_KEYRING")
if trustStoreKeyring == "" {
trustStoreKeyring, _ = os.LookupEnv("JESS_TSKEYRING")
}
}
if trustStoreKeyring != "" {
trustStore, err = truststores.NewKeyringTrustStore(trustStoreKeyring)
if err != nil {
return err
}
}
}
// requirements // requirements
if noSpec != "" { if noSpec != "" {
requirements, err = jess.ParseRequirementsFromNoSpec(noSpec) requirements, err = jess.ParseRequirementsFromNoSpec(noSpec)

View file

@ -0,0 +1,14 @@
J{
"Version": 1,
"ID": "3911c84c-78f7-4354-a7f5-0e115aa2903c",
"Scheme": "Ed25519",
"Key": "ATYVZjmhR1Zwe0KAPV99pzbzI+6zWgKvELNhFwolRdnv",
"Public": true,
"Info": {
"Name": "Safing Code Signing Cert 1",
"Owner": "",
"Created": "2022-07-11T10:23:31.705715613+02:00",
"Expires": "0001-01-01T00:00:00Z",
"Details": null
}
}

View file

@ -0,0 +1,13 @@
J{
"Version": 1,
"ID": "3911c84c-78f7-4354-a7f5-0e115aa2903c",
"Scheme": "Ed25519",
"Key": "Aee5n/V1wJM8aNpaNEPBEPeN6S0Tl41OJP0rHwtsGcZcNhVmOaFHVnB7QoA9X32nNvMj7rNaAq8Qs2EXCiVF2e8=",
"Info": {
"Name": "Safing Code Signing Cert 1",
"Owner": "",
"Created": "2022-07-11T10:23:31.705715613+02:00",
"Expires": "0001-01-01T00:00:00Z",
"Details": null
}
}

View file

@ -0,0 +1,23 @@
J{
"Version": 1,
"Name": "safing-codesign-1",
"SuiteID": "signfile_v1",
"Secrets": null,
"Senders": [
{
"Version": 1,
"ID": "3911c84c-78f7-4354-a7f5-0e115aa2903c",
"Scheme": "Ed25519",
"Key": null,
"Info": {
"Name": "Safing Code Signing Cert 1",
"Owner": "",
"Created": "2022-07-11T10:23:31.705715613+02:00",
"Expires": "0001-01-01T00:00:00Z",
"Details": null
}
}
],
"Recipients": null,
"SecurityLevel": 128
}

1
cmd/testdata/test.txt vendored Normal file
View file

@ -0,0 +1 @@
hello world!

13
cmd/testdata/test.txt.letter vendored Normal file
View file

@ -0,0 +1,13 @@
J{
"Version": 1,
"SuiteID": "signfile_v1",
"Nonce": "pKOQBQ==",
"Signatures": [
{
"Scheme": "Ed25519",
"ID": "3911c84c-78f7-4354-a7f5-0e115aa2903c",
"Value": "ftsIINZ9oApKiXYQTcLIdAZDSflp6nRN/y8Gm0rdQC+3/wal6Q+7N3N8HEAxpoxWseSQNaRVCT9hSnRQStHYBA=="
}
]
}
hello world!

9
cmd/testdata/test.txt.sig vendored Normal file
View file

@ -0,0 +1,9 @@
-----BEGIN JESS SIGNATURE-----
Q6VnVmVyc2lvbgFnU3VpdGVJRGtzaWduZmlsZV92MWVOb25jZUQb+MqAZERhdGFY
d02Dq0xhYmVsZWRIYXNoxCIJIOz3Afcn2eLXfEqkmsb7vMmXJ4rKAQvd7rlhwQz1
TUNaqFNpZ25lZEF01v9iy+uLqE1ldGFEYXRhgqd2ZXJzaW9upTAuMC4xqmlkZW50
aWZpZXKyd2luZG93cy9jb2RlL3RoaW5nalNpZ25hdHVyZXOBo2ZTY2hlbWVnRWQy
NTUxOWJJRHgkMzkxMWM4NGMtNzhmNy00MzU0LWE3ZjUtMGUxMTVhYTI5MDNjZVZh
bHVlWECJZFbIifczUGAJkmATXCHy/MiQZkiktM99X7U/cPgw3IKpKAxQsJ5LobgZ
4P2ecv0IlN4gQb+x+lycxl93E9sJ
-----END JESS SIGNATURE-----

1
cmd/testdata/test3.txt vendored Normal file
View file

@ -0,0 +1 @@
hello world!!

9
cmd/testdata/test3.txt.sig vendored Normal file
View file

@ -0,0 +1,9 @@
-----BEGIN JESS SIGNATURE-----
Q6VnVmVyc2lvbgFnU3VpdGVJRGtzaWduZmlsZV92MWVOb25jZUQJ9s/nZERhdGFY
d02Dq0xhYmVsZWRIYXNoxCIJILtKnL1AHj7YubrWdLu1D+voud8Ky04vh756eTae
rWQwqFNpZ25lZEF01v9izC6hqE1ldGFEYXRhgqd2ZXJzaW9upTAuMC4xqmlkZW50
aWZpZXKyd2luZG93cy9jb2RlL3RoaW5nalNpZ25hdHVyZXOBo2ZTY2hlbWVnRWQy
NTUxOWJJRHgkMzkxMWM4NGMtNzhmNy00MzU0LWE3ZjUtMGUxMTVhYTI5MDNjZVZh
bHVlWEBLsd2QbM7VmEsnW60hHn/V6EP2mGFauWZgbEOlKTiqumVFbWU4K7Fi91KL
Zgvwj+CNdZJ7Xv2qR7etviRDCmwC
-----END JESS SIGNATURE-----

1
cmd/testdata/test4.txt vendored Normal file
View file

@ -0,0 +1 @@
hello world!

1
cmd/testdata/testdir/test2.txt vendored Normal file
View file

@ -0,0 +1 @@
hello world!

9
cmd/testdata/testdir/test2.txt.sig vendored Normal file
View file

@ -0,0 +1,9 @@
-----BEGIN JESS SIGNATURE-----
Q6VnVmVyc2lvbgFnU3VpdGVJRGtzaWduZmlsZV92MWVOb25jZUThzxO6ZERhdGFY
d02Dq0xhYmVsZWRIYXNoxCIJIOz3Afcn2eLXfEqkmsb7vMmXJ4rKAQvd7rlhwQz1
TUNaqFNpZ25lZEF01v9izC3SqE1ldGFEYXRhgqd2ZXJzaW9upTAuMC4xqmlkZW50
aWZpZXKyd2luZG93cy9jb2RlL3RoaW5nalNpZ25hdHVyZXOBo2ZTY2hlbWVnRWQy
NTUxOWJJRHgkMzkxMWM4NGMtNzhmNy00MzU0LWE3ZjUtMGUxMTVhYTI5MDNjZVZh
bHVlWEAGLkIoej0+ilJrIyb+BzX8+Yw2LY0zkoL9vwI02/2KqKVT7/pH+LTDX1Hl
h1epYkF8ICdwa1iVNDx6P7iNmWkL
-----END JESS SIGNATURE-----

View file

@ -225,10 +225,8 @@ func TestCoreAllCombinations(t *testing.T) {
t.Logf("of these, %d were successfully detected as invalid", combinationsDetectedInvalid) t.Logf("of these, %d were successfully detected as invalid", combinationsDetectedInvalid)
} }
func testStorage(t *testing.T, suite *Suite) (detectedInvalid bool) { func testStorage(t *testing.T, suite *Suite) (detectedInvalid bool) { //nolint:thelper
t.Helper() t.Logf("testing storage with %s", suite.ID)
// t.Logf("testing storage with %s", suite.ID)
e, err := setupEnvelopeAndTrustStore(t, suite) e, err := setupEnvelopeAndTrustStore(t, suite)
if err != nil { if err != nil {
@ -378,6 +376,7 @@ func setupEnvelopeAndTrustStore(t *testing.T, suite *Suite) (*Envelope, error) {
case tools.PurposeKeyEncapsulation: case tools.PurposeKeyEncapsulation:
e.suite.Provides.Add(RecipientAuthentication) e.suite.Provides.Add(RecipientAuthentication)
case tools.PurposeSigning: case tools.PurposeSigning:
e.suite.Provides.Add(Integrity)
e.suite.Provides.Add(SenderAuthentication) e.suite.Provides.Add(SenderAuthentication)
case tools.PurposeIntegratedCipher: case tools.PurposeIntegratedCipher:
e.suite.Provides.Add(Confidentiality) e.suite.Provides.Add(Confidentiality)
@ -403,9 +402,7 @@ func setupEnvelopeAndTrustStore(t *testing.T, suite *Suite) (*Envelope, error) {
} }
// check if we are missing key derivation - this is only ok if we are merely signing // check if we are missing key derivation - this is only ok if we are merely signing
if !keyDerPresent && if !keyDerPresent && len(e.Senders) != len(e.suite.Tools) {
(len(e.suite.Provides.all) != 1 ||
!e.suite.Provides.Has(SenderAuthentication)) {
return nil, testInvalidToolset(e, "omitting a key derivation tool is only allowed when merely signing") return nil, testInvalidToolset(e, "omitting a key derivation tool is only allowed when merely signing")
} }
@ -513,9 +510,10 @@ func getOrMakeSignet(t *testing.T, tool tools.ToolLogic, recipient bool, signetI
} }
// generateCombinations returns all possible combinations of the given []string slice. // generateCombinations returns all possible combinations of the given []string slice.
// Forked from https://github.com/mxschmitt/golang-combinations/blob/a887187146560effd2677e987b069262f356297f/combinations.go //
// Copyright (c) 2018 Max Schmitt, // Forked from https://github.com/mxschmitt/golang-combinations/blob/a887187146560effd2677e987b069262f356297f/combinations.go
// MIT License. // Copyright (c) 2018 Max Schmitt,
// MIT License.
func generateCombinations(set []string) (subsets [][]string) { func generateCombinations(set []string) (subsets [][]string) {
length := uint(len(set)) length := uint(len(set))

View file

@ -3,6 +3,10 @@ package jess
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/mr-tron/base58"
"github.com/safing/portbase/formats/dsd"
) )
// Envelope holds configuration for jess to put data into a letter. // Envelope holds configuration for jess to put data into a letter.
@ -268,3 +272,80 @@ func fillPassword(signet *Signet, createPassword bool, storage TrustStore, minSe
} }
return getPasswordCallback(signet) return getPasswordCallback(signet)
} }
// CleanSignets cleans all the signets from all the non-necessary data as well
// as key material.
// This is for preparing for serializing and saving the signet.
func (e *Envelope) CleanSignets() {
for i, signet := range e.Secrets {
e.Secrets[i] = &Signet{
Version: signet.Version,
ID: signet.ID,
Scheme: signet.Scheme,
}
}
for i, signet := range e.Senders {
e.Senders[i] = &Signet{
Version: signet.Version,
ID: signet.ID,
Scheme: signet.Scheme,
}
}
for i, signet := range e.Recipients {
e.Recipients[i] = &Signet{
Version: signet.Version,
ID: signet.ID,
Scheme: signet.Scheme,
}
}
}
// ToBytes serializes the envelope to a byte slice.
func (e *Envelope) ToBytes() ([]byte, error) {
// Minimize data and remove any key material.
e.CleanSignets()
// Serialize envelope.
data, err := dsd.Dump(e, dsd.CBOR)
if err != nil {
return nil, fmt.Errorf("failed to serialize the envelope: %w", err)
}
return data, nil
}
// EnvelopeFromBytes parses and loads a serialized envelope.
func EnvelopeFromBytes(data []byte) (*Envelope, error) {
e := &Envelope{}
// Parse envelope from data.
if _, err := dsd.Load(data, e); err != nil {
return nil, fmt.Errorf("failed to parse data format: %w", err)
}
return e, nil
}
// ToBase58 serializes the envelope and encodes it with base58.
func (e *Envelope) ToBase58() (string, error) {
// Serialize Signet.
data, err := e.ToBytes()
if err != nil {
return "", err
}
// Encode and return.
return base58.Encode(data), nil
}
// EnvelopeFromBase58 parses and loads a base58 encoded serialized envelope.
func EnvelopeFromBase58(base58Encoded string) (*Envelope, error) {
// Decode string.
data, err := base58.Decode(base58Encoded)
if err != nil {
return nil, fmt.Errorf("failed to decode base58: %w", err)
}
// Parse and return.
return EnvelopeFromBytes(data)
}

123
filesig/format_armor.go Normal file
View file

@ -0,0 +1,123 @@
package filesig
import (
"bytes"
"encoding/base64"
"fmt"
"regexp"
"github.com/safing/jess"
"github.com/safing/portbase/formats/dsd"
)
const (
sigFileArmorStart = "-----BEGIN JESS SIGNATURE-----"
sigFileArmorEnd = "-----END JESS SIGNATURE-----"
sigFileLineLength = 64
)
var (
sigFileArmorFindMatcher = regexp.MustCompile(`(?ms)` + sigFileArmorStart + `(.+?)` + sigFileArmorEnd)
sigFileArmorRemoveMatcher = regexp.MustCompile(`(?ms)` + sigFileArmorStart + `.+?` + sigFileArmorEnd + `\r?\n?`)
whitespaceMatcher = regexp.MustCompile(`(?ms)\s`)
)
// ParseSigFile parses a signature file and extracts any jess signatures from it.
// If signatures are returned along with an error, the error should be treated
// as a warning, but the result should also not be treated as a full success,
// as there might be missing signatures.
func ParseSigFile(fileData []byte) (signatures []*jess.Letter, err error) {
var warning error
captured := make([][]byte, 0, 1)
// Find any signature blocks.
matches := sigFileArmorFindMatcher.FindAllSubmatch(fileData, -1)
for _, subMatches := range matches {
if len(subMatches) >= 2 {
// First entry is the whole match, second the submatch.
captured = append(
captured,
bytes.TrimPrefix(
bytes.TrimSuffix(
whitespaceMatcher.ReplaceAll(subMatches[1], nil),
[]byte(sigFileArmorEnd),
),
[]byte(sigFileArmorStart),
),
)
}
}
// Parse any found signatures.
signatures = make([]*jess.Letter, 0, len(captured))
for _, sigBase64Data := range captured {
// Decode from base64
sigData := make([]byte, base64.RawStdEncoding.DecodedLen(len(sigBase64Data)))
_, err = base64.RawStdEncoding.Decode(sigData, sigBase64Data)
if err != nil {
warning = err
continue
}
// Parse signature.
var letter *jess.Letter
letter, err = jess.LetterFromDSD(sigData)
if err != nil {
warning = err
} else {
signatures = append(signatures, letter)
}
}
return signatures, warning
}
// MakeSigFileSection creates a new section for a signature file.
func MakeSigFileSection(signature *jess.Letter) ([]byte, error) {
// Serialize.
data, err := signature.ToDSD(dsd.CBOR)
if err != nil {
return nil, fmt.Errorf("failed to serialize signature: %w", err)
}
// Encode to base64
encodedData := make([]byte, base64.RawStdEncoding.EncodedLen(len(data)))
base64.RawStdEncoding.Encode(encodedData, data)
// Split into lines and add armor.
splittedData := make([][]byte, 0, (len(encodedData)/sigFileLineLength)+3)
splittedData = append(splittedData, []byte(sigFileArmorStart))
for len(encodedData) > 0 {
if len(encodedData) > sigFileLineLength {
splittedData = append(splittedData, encodedData[:sigFileLineLength])
encodedData = encodedData[sigFileLineLength:]
} else {
splittedData = append(splittedData, encodedData)
encodedData = nil
}
}
splittedData = append(splittedData, []byte(sigFileArmorEnd))
linedData := bytes.Join(splittedData, []byte("\n"))
return linedData, nil
}
// AddToSigFile adds the given signature to the signature file.
func AddToSigFile(signature *jess.Letter, sigFileData []byte, removeExistingJessSignatures bool) (newFileData []byte, err error) {
// Create new section for new sig.
newSigSection, err := MakeSigFileSection(signature)
if err != nil {
return nil, err
}
// Remove any existing jess signature sections.
if removeExistingJessSignatures {
sigFileData = sigFileArmorRemoveMatcher.ReplaceAll(sigFileData, nil)
}
// Append new signature section to end of file with a newline.
sigFileData = append(sigFileData, []byte("\n")...)
sigFileData = append(sigFileData, newSigSection...)
return sigFileData, nil
}

View file

@ -0,0 +1,197 @@
package filesig
import (
"bytes"
"testing"
"github.com/safing/jess"
"github.com/safing/jess/lhash"
)
var (
testFileSigOneKey = "7KoUBdrRfF6drrPvKianoGfEXTQFCS5wDbfQyc87VQnYApPckRS8SfrrmAXZhV1JgKfnh44ib9nydQVEDRJiZArV22RqMfPrJmQdoAsE7zuzPRSrku8yF7zfnEv46X5GsmgfdSDrFMdG7XJd3fdaxStYCXTYDS5R"
testFileSigOneData = []byte("The quick brown fox jumps over the lazy dog")
testFileSigOneMetaData = map[string]string{
"id": "resource/path",
"version": "0.0.1",
}
testFileSigOneSignature = []byte(`
-----BEGIN JESS SIGNATURE-----
Q6VnVmVyc2lvbgFnU3VpdGVJRGdzaWduX3YxZU5vbmNlRA40a/BkRGF0YVhqTYOr
TGFiZWxlZEhhc2jEIhkgAXGM7DXNPXlt0AAg4L/stHOtI0V9Bjt17/KcD/ouWKmo
U2lnbmVkQXTW/2LH/ueoTWV0YURhdGGComlkrXJlc291cmNlL3BhdGindmVyc2lv
bqUwLjAuMWpTaWduYXR1cmVzgaNmU2NoZW1lZ0VkMjU1MTliSURwZmlsZXNpZy10
ZXN0LWtleWVWYWx1ZVhA4b1kfIJF7do6OcJnemQ5mtj/ZyMFJWWTmD1W5KvkpZac
2AP5f+dDJhzWBHsoSXTCl6uA3DA3+RbABMYAZn6eDg
-----END JESS SIGNATURE-----
`)
)
func TestFileSigFormat(t *testing.T) {
t.Parallel()
// Load test key.
signet, err := jess.SignetFromBase58(testFileSigOneKey)
if err != nil {
t.Fatal(err)
}
// Store signet.
if err := testTrustStore.StoreSignet(signet); err != nil {
t.Fatal(err)
}
// Store public key for verification.
recipient, err := signet.AsRecipient()
if err != nil {
t.Fatal(err)
}
if err := testTrustStore.StoreSignet(recipient); err != nil {
t.Fatal(err)
}
// Create envelope.
envelope := jess.NewUnconfiguredEnvelope()
envelope.SuiteID = jess.SuiteSignV1
envelope.Senders = []*jess.Signet{signet}
// Hash and sign file.
hash := lhash.Digest(lhash.BLAKE2b_256, testFileSigOneData)
letter, _, err := SignFileData(hash, testFileSigOneMetaData, envelope, testTrustStore)
if err != nil {
t.Fatal(err)
}
// Serialize signature.
sigFile, err := MakeSigFileSection(letter)
if err != nil {
t.Fatal(err)
}
// fmt.Println("Signature:")
// fmt.Println(string(sigFile))
// Parse signature again.
sigs, err := ParseSigFile(sigFile)
if err != nil {
t.Fatal(err)
}
if len(sigs) != 1 {
t.Fatalf("one sig expected, got %d", len(sigs))
}
// Verify Signature.
fileData, err := VerifyFileData(sigs[0], testFileSigOneMetaData, testTrustStore)
if err != nil {
t.Fatal(err)
}
// Verify File.
if !fileData.FileHash().MatchesData(testFileSigOneData) {
t.Fatal("file hash does not match")
}
// Verify the saved version of the signature.
// Parse the saved signature.
sigs, err = ParseSigFile(testFileSigOneSignature)
if err != nil {
t.Fatal(err)
}
if len(sigs) != 1 {
t.Fatalf("only one sig expected, got %d", len(sigs))
}
// Verify Signature.
fileData, err = VerifyFileData(sigs[0], testFileSigOneMetaData, testTrustStore)
if err != nil {
t.Fatal(err)
}
// Verify File.
if !fileData.FileHash().MatchesData(testFileSigOneData) {
t.Fatal("file hash does not match")
}
}
var (
testFileSigFormat1 = []byte(`TGFiZWxlZEhhc2jEIhkgAXGM7DXNPXlt0AAg4L
-----BEGIN JESS SIGNATURE-----
Q6VnVmVyc2lvbgFnU3VpdGVJRGdzaWduX3YxZU5vbmNlRA40a/BkRGF0YVhqTYOr
TGFiZWxlZEhhc2jEIhkgAXGM7DXNPXlt0AAg4L/stHOtI0V9Bjt17/KcD/ouWKmo
U2lnbmVkQXTW/2LH/ueoTWV0YURhdGGComlkrXJlc291cmNlL3BhdGindmVyc2lv
bqUwLjAuMWpTaWduYXR1cmVzgaNmU2NoZW1lZ0VkMjU1MTliSURwZmlsZXNpZy10
ZXN0LWtleWVWYWx1ZVhA4b1kfIJF7do6OcJnemQ5mtj/ZyMFJWWTmD1W5KvkpZac
2AP5f+dDJhzWBHsoSXTCl6uA3DA3+RbABMYAZn6eDg
-----END JESS SIGNATURE-----
-----END JESS SIGNATURE-----
-----BEGIN JESS SIGNATURE-----
Q6VnVmVyc2lvbgFnU3VpdGVJRGdzaWduX3YxZU5vbmNlRA40a/BkRGF0YVhqTYOr
TGFiZWxlZEhhc2jEIhkgAXGM7DXNPXlt0AAg4L/stHOtI0V9Bjt17/KcD/ouWKmo
U2lnbmVkQXTW/2LH/ueoTWV0YURhdGGComlk
rXJlc291cmNlL3BhdGindmVyc2lvbqUwLjAuMWpTaWduYXR1cmVzgaNmU2NoZW1lZ0VkMjU1MTliSURwZmlsZXNpZy10
ZXN0LWtleWVWYWx1ZVhA4b1kfIJF7do6OcJnemQ5mtj/ZyMFJWWTmD1W5KvkpZac
2AP5f+dDJhzWBHsoSXTCl6uA3DA3+RbABMYAZn6eDg
-----END JESS SIGNATURE-----
end`)
testFileSigFormat2 = []byte(`test data 1
-----BEGIN JESS SIGNATURE-----
invalid sig
-----END JESS SIGNATURE-----
test data 2`)
testFileSigFormat3 = []byte(`test data 1
-----BEGIN JESS SIGNATURE-----
invalid sig
-----END JESS SIGNATURE-----
test data 2
-----BEGIN JESS SIGNATURE-----
Q6VnVmVyc2lvbgFnU3VpdGVJRGdzaWduX3YxZU5vbmNlRA40a/BkRGF0YVhqTYOr
TGFiZWxlZEhhc2jEIhkgAXGM7DXNPXlt0AAg4L/stHOtI0V9Bjt17/KcD/ouWKmo
U2lnbmVkQXTW/2LH/ueoTWV0YURhdGGComlkrXJlc291cmNlL3BhdGindmVyc2lv
bqUwLjAuMWpTaWduYXR1cmVzgaNmU2NoZW1lZ0VkMjU1MTliSURwZmlsZXNpZy10
ZXN0LWtleWVWYWx1ZVhA4b1kfIJF7do6OcJnemQ5mtj/ZyMFJWWTmD1W5KvkpZac
2AP5f+dDJhzWBHsoSXTCl6uA3DA3+RbABMYAZn6eDg
-----END JESS SIGNATURE-----`)
testFileSigFormat4 = []byte(`test data 1
test data 2
-----BEGIN JESS SIGNATURE-----
Q6VnVmVyc2lvbgFnU3VpdGVJRGdzaWduX3YxZU5vbmNlRA40a/BkRGF0YVhqTYOr
TGFiZWxlZEhhc2jEIhkgAXGM7DXNPXlt0AAg4L/stHOtI0V9Bjt17/KcD/ouWKmo
U2lnbmVkQXTW/2LH/ueoTWV0YURhdGGComlkrXJlc291cmNlL3BhdGindmVyc2lv
bqUwLjAuMWpTaWduYXR1cmVzgaNmU2NoZW1lZ0VkMjU1MTliSURwZmlsZXNpZy10
ZXN0LWtleWVWYWx1ZVhA4b1kfIJF7do6OcJnemQ5mtj/ZyMFJWWTmD1W5KvkpZac
2AP5f+dDJhzWBHsoSXTCl6uA3DA3+RbABMYAZn6eDg
-----END JESS SIGNATURE-----`)
)
func TestFileSigFormatParsing(t *testing.T) {
t.Parallel()
sigs, err := ParseSigFile(testFileSigFormat1)
if err != nil {
t.Fatal(err)
}
if len(sigs) != 2 {
t.Fatalf("expected two signatures, got %d", 1)
}
newFile, err := AddToSigFile(sigs[0], testFileSigFormat2, false)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(newFile, testFileSigFormat3) {
t.Fatalf("unexpected output:\n%s", string(newFile))
}
newFile, err = AddToSigFile(sigs[0], testFileSigFormat2, true)
if err != nil {
t.Fatal(err)
}
if !bytes.Equal(newFile, testFileSigFormat4) {
t.Fatalf("unexpected output:\n%s", string(newFile))
}
}

146
filesig/helpers.go Normal file
View file

@ -0,0 +1,146 @@
package filesig
import (
"errors"
"fmt"
"os"
"strings"
"github.com/safing/jess"
"github.com/safing/jess/hashtools"
"github.com/safing/jess/lhash"
)
// SignFile signs a file and replaces the signature file with a new one.
// If the dataFilePath is "-", the file data is read from stdin.
// Existing jess signatures in the signature file are removed.
func SignFile(dataFilePath, signatureFilePath string, metaData map[string]string, envelope *jess.Envelope, trustStore jess.TrustStore) (fileData *FileData, err error) {
// Load encryption suite.
if err := envelope.LoadSuite(); err != nil {
return nil, err
}
// Extract the used hashing algorithm from the suite.
var hashTool *hashtools.HashTool
for _, toolID := range envelope.Suite().Tools {
if strings.Contains(toolID, "(") {
hashToolID := strings.Trim(strings.Split(toolID, "(")[1], "()")
hashTool, _ = hashtools.Get(hashToolID)
break
}
}
if hashTool == nil {
return nil, errors.New("suite not suitable for file signing")
}
// Hash the data file.
var fileHash *lhash.LabeledHash
if dataFilePath == "-" {
fileHash, err = hashTool.LabeledHasher().DigestFromReader(os.Stdin)
} else {
fileHash, err = hashTool.LabeledHasher().DigestFile(dataFilePath)
}
if err != nil {
return nil, fmt.Errorf("failed to hash file: %w", err)
}
// Sign the file data.
signature, fileData, err := SignFileData(fileHash, metaData, envelope, trustStore)
if err != nil {
return nil, fmt.Errorf("failed to sign file: %w", err)
}
sigFileData, err := os.ReadFile(signatureFilePath)
var newSigFileData []byte
switch {
case err == nil:
// Add signature to existing file.
newSigFileData, err = AddToSigFile(signature, sigFileData, true)
if err != nil {
return nil, fmt.Errorf("failed to add signature to file: %w", err)
}
case os.IsNotExist(err):
// Make signature section for saving to disk.
newSigFileData, err = MakeSigFileSection(signature)
if err != nil {
return nil, fmt.Errorf("failed to format signature for file: %w", err)
}
default:
return nil, fmt.Errorf("failed to open existing signature file: %w", err)
}
// Write the signature to file.
if err := os.WriteFile(signatureFilePath, newSigFileData, 0o0644); err != nil { //nolint:gosec
return nil, fmt.Errorf("failed to write signature to file: %w", err)
}
return fileData, nil
}
// VerifyFile verifies the given files and returns the verified file data.
// If the dataFilePath is "-", the file data is read from stdin.
// If an error is returned, there was an error in at least some part of the process.
// Any returned file data struct must be checked for an verification error.
func VerifyFile(dataFilePath, signatureFilePath string, metaData map[string]string, trustStore jess.TrustStore) (verifiedFileData []*FileData, err error) {
var lastErr error
// Read signature from file.
sigFileData, err := os.ReadFile(signatureFilePath)
if err != nil {
return nil, fmt.Errorf("failed to read signature file: %w", err)
}
// Extract all signatures.
sigs, err := ParseSigFile(sigFileData)
switch {
case len(sigs) == 0 && err != nil:
return nil, fmt.Errorf("failed to parse signature file: %w", err)
case len(sigs) == 0:
return nil, errors.New("no signatures found in signature file")
case err != nil:
lastErr = fmt.Errorf("failed to parse signature file: %w", err)
}
// Verify all signatures.
goodFileData := make([]*FileData, 0, len(sigs))
var badFileData []*FileData
for _, sigLetter := range sigs {
// Verify signature.
fileData, err := VerifyFileData(sigLetter, metaData, trustStore)
if err != nil {
lastErr = err
if fileData != nil {
fileData.verificationError = err
badFileData = append(badFileData, fileData)
}
continue
}
// Hash the file.
var fileHash *lhash.LabeledHash
if dataFilePath == "-" {
fileHash, err = fileData.FileHash().Algorithm().DigestFromReader(os.Stdin)
} else {
fileHash, err = fileData.FileHash().Algorithm().DigestFile(dataFilePath)
}
if err != nil {
lastErr = err
fileData.verificationError = err
badFileData = append(badFileData, fileData)
continue
}
// Check if the hash matches.
if !fileData.FileHash().Equal(fileHash) {
lastErr = errors.New("signature invalid: file was modified")
fileData.verificationError = lastErr
badFileData = append(badFileData, fileData)
continue
}
// Add verified file data to list for return.
goodFileData = append(goodFileData, fileData)
}
return append(goodFileData, badFileData...), lastErr
}

123
filesig/main.go Normal file
View file

@ -0,0 +1,123 @@
package filesig
import (
"fmt"
"time"
"github.com/safing/jess"
"github.com/safing/jess/lhash"
"github.com/safing/portbase/formats/dsd"
)
// Extension holds the default file extension to be used for signature files.
const Extension = ".sig"
var fileSigRequirements = jess.NewRequirements().
Remove(jess.RecipientAuthentication).
Remove(jess.Confidentiality)
// FileData describes a file that is signed.
type FileData struct {
LabeledHash []byte
fileHash *lhash.LabeledHash
SignedAt time.Time
MetaData map[string]string
signature *jess.Letter
verificationError error
}
// FileHash returns the labeled hash of the file that was signed.
func (fd *FileData) FileHash() *lhash.LabeledHash {
return fd.fileHash
}
// Signature returns the signature, if present.
func (fd *FileData) Signature() *jess.Letter {
return fd.signature
}
// VerificationError returns the error encountered during verification.
func (fd *FileData) VerificationError() error {
return fd.verificationError
}
// SignFileData signs the given file checksum and metadata.
func SignFileData(fileHash *lhash.LabeledHash, metaData map[string]string, envelope *jess.Envelope, trustStore jess.TrustStore) (letter *jess.Letter, fd *FileData, err error) {
// Create session.
session, err := envelope.Correspondence(trustStore)
if err != nil {
return nil, nil, err
}
// Check if the envelope is suitable for signing.
if err := envelope.Suite().Provides.CheckComplianceTo(fileSigRequirements); err != nil {
return nil, nil, fmt.Errorf("envelope not suitable for signing")
}
// Create struct and transform data into serializable format to be signed.
fd = &FileData{
SignedAt: time.Now().Truncate(time.Second),
fileHash: fileHash,
MetaData: metaData,
}
fd.LabeledHash = fd.fileHash.Bytes()
// Serialize file signature.
fileData, err := dsd.Dump(fd, dsd.MsgPack)
if err != nil {
return nil, nil, fmt.Errorf("failed to serialize file signature data: %w", err)
}
// Sign data.
letter, err = session.Close(fileData)
if err != nil {
return nil, nil, fmt.Errorf("failed to sign: %w", err)
}
return letter, fd, nil
}
// VerifyFileData verifies the given signed file data and returns the file data.
// If an error is returned, there was an error in at least some part of the process.
// Any returned file data struct must be checked for an verification error.
func VerifyFileData(letter *jess.Letter, requiredMetaData map[string]string, trustStore jess.TrustStore) (fd *FileData, err error) {
// Parse data.
fd = &FileData{
signature: letter,
}
_, err = dsd.Load(letter.Data, fd)
if err != nil {
return nil, fmt.Errorf("failed to parse file signature data: %w", err)
}
// Verify signature and get data.
_, err = letter.Open(fileSigRequirements, trustStore)
if err != nil {
fd.verificationError = fmt.Errorf("failed to verify file signature: %w", err)
return fd, fd.verificationError
}
// Check if the required metadata matches.
for reqKey, reqValue := range requiredMetaData {
sigMetaValue, ok := fd.MetaData[reqKey]
if !ok {
fd.verificationError = fmt.Errorf("missing required metadata key %q", reqKey)
return fd, fd.verificationError
}
if sigMetaValue != reqValue {
fd.verificationError = fmt.Errorf("required metadata %q=%q does not match the file's value %q", reqKey, reqValue, sigMetaValue)
return fd, fd.verificationError
}
}
// Parse labeled hash.
fd.fileHash, err = lhash.Load(fd.LabeledHash)
if err != nil {
fd.verificationError = fmt.Errorf("failed to parse file checksum: %w", err)
return fd, fd.verificationError
}
return fd, nil
}

130
filesig/main_test.go Normal file
View file

@ -0,0 +1,130 @@
package filesig
import (
"errors"
"testing"
"time"
"github.com/safing/jess"
"github.com/safing/jess/lhash"
"github.com/safing/jess/tools"
)
var (
testTrustStore = jess.NewMemTrustStore()
testData1 = "The quick brown fox jumps over the lazy dog. "
testFileSigMetaData1 = map[string]string{
"key1": "value1",
"key2": "value2",
}
testFileSigMetaData1x = map[string]string{
"key1": "value1x",
}
testFileSigMetaData2 = map[string]string{
"key3": "value3",
"key4": "value4",
}
testFileSigMetaData3 = map[string]string{}
)
func TestFileSigs(t *testing.T) {
t.Parallel()
testFileSigningWithOptions(t, testFileSigMetaData1, testFileSigMetaData1, true)
testFileSigningWithOptions(t, testFileSigMetaData1, testFileSigMetaData1x, false)
testFileSigningWithOptions(t, testFileSigMetaData2, testFileSigMetaData2, true)
testFileSigningWithOptions(t, testFileSigMetaData1, testFileSigMetaData2, false)
testFileSigningWithOptions(t, testFileSigMetaData2, testFileSigMetaData1, false)
testFileSigningWithOptions(t, testFileSigMetaData1, testFileSigMetaData3, true)
testFileSigningWithOptions(t, testFileSigMetaData3, testFileSigMetaData1, false)
}
func testFileSigningWithOptions(t *testing.T, signingMetaData, verifyingMetaData map[string]string, shouldSucceed bool) {
t.Helper()
// Get tool for key generation.
tool, err := tools.Get("Ed25519")
if err != nil {
t.Fatal(err)
}
// Generate key pair.
s, err := getOrMakeSignet(t, tool.StaticLogic, false, "test-key-filesig-1")
if err != nil {
t.Fatal(err)
}
// Hash "file".
fileHash := lhash.BLAKE2b_256.Digest([]byte(testData1))
// Make envelope.
envelope := jess.NewUnconfiguredEnvelope()
envelope.SuiteID = jess.SuiteSignV1
envelope.Senders = []*jess.Signet{s}
// Sign data.
letter, fileData, err := SignFileData(fileHash, signingMetaData, envelope, testTrustStore)
if err != nil {
t.Fatal(err)
}
// Check if the checksum made it.
if len(fileData.LabeledHash) == 0 {
t.Fatal("missing labeled hash")
}
// Verify signature.
_, err = VerifyFileData(letter, verifyingMetaData, testTrustStore)
if (err == nil) != shouldSucceed {
t.Fatal(err)
}
}
func getOrMakeSignet(t *testing.T, tool tools.ToolLogic, recipient bool, signetID string) (*jess.Signet, error) {
t.Helper()
// check if signet already exists
signet, err := testTrustStore.GetSignet(signetID, recipient)
if err == nil {
return signet, nil
}
// handle special cases
if tool == nil {
return nil, errors.New("bad parameters")
}
// create new signet
newSignet := jess.NewSignetBase(tool.Definition())
newSignet.ID = signetID
// generate signet and log time taken
start := time.Now()
err = tool.GenerateKey(newSignet)
if err != nil {
return nil, err
}
t.Logf("generated %s signet %s in %s", newSignet.Scheme, newSignet.ID, time.Since(start))
// store signet
err = testTrustStore.StoreSignet(newSignet)
if err != nil {
return nil, err
}
// store recipient
newRcpt, err := newSignet.AsRecipient()
if err != nil {
return nil, err
}
err = testTrustStore.StoreSignet(newRcpt)
if err != nil {
return nil, err
}
// return
if recipient {
return newRcpt, nil
}
return newSignet, nil
}

1
go.mod
View file

@ -12,6 +12,7 @@ require (
github.com/satori/go.uuid v1.2.0 github.com/satori/go.uuid v1.2.0
github.com/spf13/cobra v1.5.0 github.com/spf13/cobra v1.5.0
github.com/tevino/abool v1.2.0 github.com/tevino/abool v1.2.0
github.com/zalando/go-keyring v0.2.1 // indirect
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
) )

8
go.sum
View file

@ -71,6 +71,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/andyleap/gencode v0.0.0-20171124163308-e1423834d4b4/go.mod h1:yE6zprmDWRrIsbjHdb+C3MGq+YpJnqJxaFilOM27PtI= github.com/andyleap/gencode v0.0.0-20171124163308-e1423834d4b4/go.mod h1:yE6zprmDWRrIsbjHdb+C3MGq+YpJnqJxaFilOM27PtI=
github.com/andyleap/parser v0.0.0-20160126201130-db5a13a7cd46/go.mod h1:optl5aMZUO+oj3KCDaQ0WYQMP6QhUQXXDAHQnCA3wI8= github.com/andyleap/parser v0.0.0-20160126201130-db5a13a7cd46/go.mod h1:optl5aMZUO+oj3KCDaQ0WYQMP6QhUQXXDAHQnCA3wI8=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@ -122,6 +124,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsr
github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
github.com/danieljoos/wincred v1.1.0 h1:3RNcEpBg4IhIChZdFRSdlQt1QjCp1sMAPIrOnm7Yf8g=
github.com/danieljoos/wincred v1.1.0/go.mod h1:XYlo+eRTsVA9aHGp7NGjFkPla4m+DCL7hqDjlFjiygg=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -164,6 +168,8 @@ github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro=
github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
@ -507,6 +513,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zalando/go-keyring v0.2.1 h1:MBRN/Z8H4U5wEKXiD67YbDAr5cj/DOStmSga70/2qKc=
github.com/zalando/go-keyring v0.2.1/go.mod h1:g63M2PPn0w5vjmEbwAX3ib5I+41zdm4esSETOn9Y6Dw=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4= go.etcd.io/bbolt v1.3.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=

View file

@ -6,6 +6,8 @@ import (
// Register BLAKE2 in Go's internal registry. // Register BLAKE2 in Go's internal registry.
_ "golang.org/x/crypto/blake2b" _ "golang.org/x/crypto/blake2b"
_ "golang.org/x/crypto/blake2s" _ "golang.org/x/crypto/blake2s"
"github.com/safing/jess/lhash"
) )
func init() { func init() {
@ -21,6 +23,7 @@ func init() {
BlockSize: crypto.BLAKE2s_256.New().BlockSize(), BlockSize: crypto.BLAKE2s_256.New().BlockSize(),
SecurityLevel: 128, SecurityLevel: 128,
Comment: "RFC 7693, successor of SHA3 finalist, optimized for 8-32 bit software", Comment: "RFC 7693, successor of SHA3 finalist, optimized for 8-32 bit software",
labeledAlg: lhash.BLAKE2s_256,
})) }))
Register(blake2bBase.With(&HashTool{ Register(blake2bBase.With(&HashTool{
Name: "BLAKE2b-256", Name: "BLAKE2b-256",
@ -28,6 +31,7 @@ func init() {
DigestSize: crypto.BLAKE2b_256.Size(), DigestSize: crypto.BLAKE2b_256.Size(),
BlockSize: crypto.BLAKE2b_256.New().BlockSize(), BlockSize: crypto.BLAKE2b_256.New().BlockSize(),
SecurityLevel: 128, SecurityLevel: 128,
labeledAlg: lhash.BLAKE2b_256,
})) }))
Register(blake2bBase.With(&HashTool{ Register(blake2bBase.With(&HashTool{
Name: "BLAKE2b-384", Name: "BLAKE2b-384",
@ -35,6 +39,7 @@ func init() {
DigestSize: crypto.BLAKE2b_384.Size(), DigestSize: crypto.BLAKE2b_384.Size(),
BlockSize: crypto.BLAKE2b_384.New().BlockSize(), BlockSize: crypto.BLAKE2b_384.New().BlockSize(),
SecurityLevel: 192, SecurityLevel: 192,
labeledAlg: lhash.BLAKE2b_384,
})) }))
Register(blake2bBase.With(&HashTool{ Register(blake2bBase.With(&HashTool{
Name: "BLAKE2b-512", Name: "BLAKE2b-512",
@ -42,5 +47,6 @@ func init() {
DigestSize: crypto.BLAKE2b_512.Size(), DigestSize: crypto.BLAKE2b_512.Size(),
BlockSize: crypto.BLAKE2b_512.New().BlockSize(), BlockSize: crypto.BLAKE2b_512.New().BlockSize(),
SecurityLevel: 256, SecurityLevel: 256,
labeledAlg: lhash.BLAKE2b_512,
})) }))
} }

View file

@ -3,6 +3,8 @@ package hashtools
import ( import (
"crypto" "crypto"
"hash" "hash"
"github.com/safing/jess/lhash"
) )
// HashTool holds generic information about a hash tool. // HashTool holds generic information about a hash tool.
@ -16,6 +18,8 @@ type HashTool struct {
Comment string Comment string
Author string Author string
labeledAlg lhash.Algorithm
} }
// New returns a new hash.Hash instance of the hash tool. // New returns a new hash.Hash instance of the hash tool.
@ -46,6 +50,14 @@ func (ht *HashTool) With(changes *HashTool) *HashTool {
if changes.Author == "" { if changes.Author == "" {
changes.Author = ht.Author changes.Author = ht.Author
} }
if changes.labeledAlg == 0 {
changes.labeledAlg = ht.labeledAlg
}
return changes return changes
} }
// LabeledHasher returns the corresponding labeled hashing algorithm.
func (ht *HashTool) LabeledHasher() lhash.Algorithm {
return ht.labeledAlg
}

View file

@ -2,13 +2,14 @@ package hashtools
import ( import (
"crypto" "crypto"
// Register SHA2 in Go's internal registry. // Register SHA2 in Go's internal registry.
_ "crypto/sha256" _ "crypto/sha256"
_ "crypto/sha512" _ "crypto/sha512"
// Register SHA3 in Go's internal registry. // Register SHA3 in Go's internal registry.
_ "golang.org/x/crypto/sha3" _ "golang.org/x/crypto/sha3"
"github.com/safing/jess/lhash"
) )
func init() { func init() {
@ -24,6 +25,7 @@ func init() {
BlockSize: crypto.SHA224.New().BlockSize(), BlockSize: crypto.SHA224.New().BlockSize(),
SecurityLevel: 112, SecurityLevel: 112,
Author: "NSA, 2004", Author: "NSA, 2004",
labeledAlg: lhash.SHA2_224,
})) }))
Register(sha2Base.With(&HashTool{ Register(sha2Base.With(&HashTool{
Name: "SHA2-256", Name: "SHA2-256",
@ -31,6 +33,7 @@ func init() {
DigestSize: crypto.SHA256.Size(), DigestSize: crypto.SHA256.Size(),
BlockSize: crypto.SHA256.New().BlockSize(), BlockSize: crypto.SHA256.New().BlockSize(),
SecurityLevel: 128, SecurityLevel: 128,
labeledAlg: lhash.SHA2_256,
})) }))
Register(sha2Base.With(&HashTool{ Register(sha2Base.With(&HashTool{
Name: "SHA2-384", Name: "SHA2-384",
@ -38,6 +41,7 @@ func init() {
DigestSize: crypto.SHA384.Size(), DigestSize: crypto.SHA384.Size(),
BlockSize: crypto.SHA384.New().BlockSize(), BlockSize: crypto.SHA384.New().BlockSize(),
SecurityLevel: 192, SecurityLevel: 192,
labeledAlg: lhash.SHA2_384,
})) }))
Register(sha2Base.With(&HashTool{ Register(sha2Base.With(&HashTool{
Name: "SHA2-512", Name: "SHA2-512",
@ -45,6 +49,7 @@ func init() {
DigestSize: crypto.SHA512.Size(), DigestSize: crypto.SHA512.Size(),
BlockSize: crypto.SHA512.New().BlockSize(), BlockSize: crypto.SHA512.New().BlockSize(),
SecurityLevel: 256, SecurityLevel: 256,
labeledAlg: lhash.SHA2_512,
})) }))
Register(sha2Base.With(&HashTool{ Register(sha2Base.With(&HashTool{
Name: "SHA2-512-224", Name: "SHA2-512-224",
@ -52,6 +57,7 @@ func init() {
DigestSize: crypto.SHA512_224.Size(), DigestSize: crypto.SHA512_224.Size(),
BlockSize: crypto.SHA512_224.New().BlockSize(), BlockSize: crypto.SHA512_224.New().BlockSize(),
SecurityLevel: 112, SecurityLevel: 112,
labeledAlg: lhash.SHA2_512_224,
})) }))
Register(sha2Base.With(&HashTool{ Register(sha2Base.With(&HashTool{
Name: "SHA2-512-256", Name: "SHA2-512-256",
@ -59,6 +65,7 @@ func init() {
DigestSize: crypto.SHA512_256.Size(), DigestSize: crypto.SHA512_256.Size(),
BlockSize: crypto.SHA512_256.New().BlockSize(), BlockSize: crypto.SHA512_256.New().BlockSize(),
SecurityLevel: 128, SecurityLevel: 128,
labeledAlg: lhash.SHA2_512_256,
})) }))
// SHA3 // SHA3
@ -72,6 +79,7 @@ func init() {
DigestSize: crypto.SHA3_224.Size(), DigestSize: crypto.SHA3_224.Size(),
BlockSize: crypto.SHA3_224.New().BlockSize(), BlockSize: crypto.SHA3_224.New().BlockSize(),
SecurityLevel: 112, SecurityLevel: 112,
labeledAlg: lhash.SHA3_224,
})) }))
Register(sha3Base.With(&HashTool{ Register(sha3Base.With(&HashTool{
Name: "SHA3-256", Name: "SHA3-256",
@ -79,6 +87,7 @@ func init() {
DigestSize: crypto.SHA3_256.Size(), DigestSize: crypto.SHA3_256.Size(),
BlockSize: crypto.SHA3_256.New().BlockSize(), BlockSize: crypto.SHA3_256.New().BlockSize(),
SecurityLevel: 128, SecurityLevel: 128,
labeledAlg: lhash.SHA3_256,
})) }))
Register(sha3Base.With(&HashTool{ Register(sha3Base.With(&HashTool{
Name: "SHA3-384", Name: "SHA3-384",
@ -86,6 +95,7 @@ func init() {
DigestSize: crypto.SHA3_384.Size(), DigestSize: crypto.SHA3_384.Size(),
BlockSize: crypto.SHA3_384.New().BlockSize(), BlockSize: crypto.SHA3_384.New().BlockSize(),
SecurityLevel: 192, SecurityLevel: 192,
labeledAlg: lhash.SHA3_384,
})) }))
Register(sha3Base.With(&HashTool{ Register(sha3Base.With(&HashTool{
Name: "SHA3-512", Name: "SHA3-512",
@ -93,5 +103,6 @@ func init() {
DigestSize: crypto.SHA3_512.Size(), DigestSize: crypto.SHA3_512.Size(),
BlockSize: crypto.SHA3_512.New().BlockSize(), BlockSize: crypto.SHA3_512.New().BlockSize(),
SecurityLevel: 256, SecurityLevel: 256,
labeledAlg: lhash.SHA3_512,
})) }))
} }

195
import_export.go Normal file
View file

@ -0,0 +1,195 @@
package jess
import (
"errors"
"fmt"
"regexp"
"strings"
)
// Keywords and Prefixes for the export text format.
const (
ExportSenderKeyword = "sender"
ExportSenderPrefix = "sender:"
ExportRecipientKeyword = "recipient"
ExportRecipientPrefix = "recipient:"
ExportKeyKeyword = "secret"
ExportKeyPrefix = "secret:"
ExportEnvelopeKeyword = "envelope"
ExportEnvelopePrefix = "envelope:"
)
// Export exports the public part of a signet in text format.
func (signet *Signet) Export(short bool) (textFormat string, err error) {
// Make public if needed.
if !signet.Public {
signet, err = signet.AsRecipient()
if err != nil {
return "", err
}
}
// Transform to text format.
return signet.toTextFormat(short)
}
// Backup exports the private part of a signet in text format.
func (signet *Signet) Backup(short bool) (textFormat string, err error) {
// Abprt if public.
if signet.Public {
return "", errors.New("cannot backup (only export) a recipient")
}
// Transform to text format.
return signet.toTextFormat(short)
}
func (signet *Signet) toTextFormat(short bool) (textFormat string, err error) {
// Serialize to base58.
base58data, err := signet.ToBase58()
if err != nil {
return "", err
}
// Define keywords.
var keyword, typeComment string
switch {
case signet.Scheme == SignetSchemePassword:
return "", errors.New("cannot backup or export passwords")
case signet.Scheme == SignetSchemeKey:
// Check if the signet is marked as "public".
if signet.Public {
return "", errors.New("cannot export keys")
}
keyword = ExportKeyKeyword
typeComment = "symmetric-key"
case signet.Public:
keyword = ExportRecipientKeyword
typeComment = fmt.Sprintf(
"public-%s-key", toTextFormatString(signet.Scheme),
)
default:
keyword = ExportSenderKeyword
typeComment = fmt.Sprintf(
"private-%s-key", toTextFormatString(signet.Scheme),
)
}
// Transform to text format.
if short {
return fmt.Sprintf(
"%s:%s",
keyword,
base58data,
), nil
}
return fmt.Sprintf(
"%s:%s:%s:%s",
keyword,
typeComment,
toTextFormatString(signet.Info.Name),
base58data,
), nil
}
// Export exports the envelope in text format.
func (e *Envelope) Export(short bool) (textFormat string, err error) {
// Remove and key data.
e.CleanSignets()
// Serialize to base58.
base58data, err := e.ToBase58()
if err != nil {
return "", err
}
// Transform to text format.
if short {
return fmt.Sprintf(
"%s:%s",
ExportEnvelopeKeyword,
base58data,
), nil
}
return fmt.Sprintf(
"%s:%s:%s:%s",
ExportEnvelopeKeyword,
e.SuiteID,
e.Name,
base58data,
), nil
}
// KeyFromTextFormat loads a secret key from the text format.
func KeyFromTextFormat(textFormat string) (*Signet, error) {
// Check the identifier.
if !strings.HasPrefix(textFormat, ExportKeyPrefix) {
return nil, errors.New("not a secret")
}
// Parse the data section.
splitted := strings.Split(textFormat, ":")
if len(splitted) < 2 {
return nil, errors.New("invalid format")
}
return SignetFromBase58(splitted[len(splitted)-1])
}
// SenderFromTextFormat loads a sender (private key) from the text format.
func SenderFromTextFormat(textFormat string) (*Signet, error) {
// Check the identifier.
if !strings.HasPrefix(textFormat, ExportSenderPrefix) {
return nil, errors.New("not a sender")
}
// Parse the data section.
splitted := strings.Split(textFormat, ":")
if len(splitted) < 2 {
return nil, errors.New("invalid format")
}
return SignetFromBase58(splitted[len(splitted)-1])
}
// RecipientFromTextFormat loads a recipient (public key) from the text format.
func RecipientFromTextFormat(textFormat string) (*Signet, error) {
// Check the identifier.
if !strings.HasPrefix(textFormat, ExportRecipientPrefix) {
return nil, errors.New("not a recipient")
}
// Parse the data section.
splitted := strings.Split(textFormat, ":")
if len(splitted) < 2 {
return nil, errors.New("invalid format")
}
return SignetFromBase58(splitted[len(splitted)-1])
}
// EnvelopeFromTextFormat loads an envelope from the text format.
func EnvelopeFromTextFormat(textFormat string) (*Envelope, error) {
// Check the identifier.
if !strings.HasPrefix(textFormat, ExportEnvelopePrefix) {
return nil, errors.New("not an envelope")
}
// Parse the data section.
splitted := strings.Split(textFormat, ":")
if len(splitted) < 2 {
return nil, errors.New("invalid format")
}
return EnvelopeFromBase58(splitted[len(splitted)-1])
}
var replaceForTextFormatMatcher = regexp.MustCompile(`[^A-Za-z0-9]+`)
// toTextFormatString makes a string compatible with the text format.
func toTextFormatString(s string) string {
return strings.ToLower(
strings.Trim(
replaceForTextFormatMatcher.ReplaceAllString(s, "-"), "-",
),
)
}

View file

@ -6,6 +6,7 @@ package lhash
import ( import (
"crypto" "crypto"
"hash" "hash"
"io"
// Register SHA2 in Go's internal registry. // Register SHA2 in Go's internal registry.
_ "crypto/sha256" _ "crypto/sha256"
@ -83,3 +84,65 @@ func (a Algorithm) new() hash.Hash {
return nil return nil
} }
} }
func (a Algorithm) String() string {
switch a {
// SHA2
case SHA2_224:
return "SHA2_224"
case SHA2_256:
return "SHA2_256"
case SHA2_384:
return "SHA2_384"
case SHA2_512:
return "SHA2_512"
case SHA2_512_224:
return "SHA2_512_224"
case SHA2_512_256:
return "SHA2_512_256"
// SHA3
case SHA3_224:
return "SHA3_224"
case SHA3_256:
return "SHA3_256"
case SHA3_384:
return "SHA3_384"
case SHA3_512:
return "SHA3_512"
// BLAKE2
case BLAKE2s_256:
return "BLAKE2s_256"
case BLAKE2b_256:
return "BLAKE2b_256"
case BLAKE2b_384:
return "BLAKE2b_384"
case BLAKE2b_512:
return "BLAKE2b_512"
default:
return "unknown"
}
}
// RawHasher returns a new raw hasher of the algorithm.
func (a Algorithm) RawHasher() hash.Hash {
return a.new()
}
// Digest creates a new labeled hash and digests the given data.
func (a Algorithm) Digest(data []byte) *LabeledHash {
return Digest(a, data)
}
// DigestFile creates a new labeled hash and digests the given file.
func (a Algorithm) DigestFile(pathToFile string) (*LabeledHash, error) {
return DigestFile(a, pathToFile)
}
// DigestFromReader creates a new labeled hash and digests from the given reader.
func (a Algorithm) DigestFromReader(reader io.Reader) (*LabeledHash, error) {
return DigestFromReader(a, reader)
}

View file

@ -1,11 +1,14 @@
package lhash package lhash
import ( import (
"bufio"
"crypto/subtle" "crypto/subtle"
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"errors" "errors"
"fmt" "fmt"
"io"
"os"
"github.com/mr-tron/base58" "github.com/mr-tron/base58"
@ -21,8 +24,8 @@ type LabeledHash struct {
// Digest creates a new labeled hash and digests the given data. // Digest creates a new labeled hash and digests the given data.
func Digest(alg Algorithm, data []byte) *LabeledHash { func Digest(alg Algorithm, data []byte) *LabeledHash {
hasher := alg.new() hasher := alg.new()
_, _ = hasher.Write(data) // never returns an error _, _ = hasher.Write(data) // Never returns an error.
defer hasher.Reset() // internal state may leak data if kept in memory defer hasher.Reset() // Internal state may leak data if kept in memory.
return &LabeledHash{ return &LabeledHash{
alg: alg, alg: alg,
@ -30,6 +33,34 @@ func Digest(alg Algorithm, data []byte) *LabeledHash {
} }
} }
// DigestFile creates a new labeled hash and digests the given file.
func DigestFile(alg Algorithm, pathToFile string) (*LabeledHash, error) {
// Open file that should be hashed.
file, err := os.Open(pathToFile)
if err != nil {
return nil, fmt.Errorf("failed to open file: %w", err)
}
return DigestFromReader(alg, file)
}
// DigestFromReader creates a new labeled hash and digests from the given reader.
func DigestFromReader(alg Algorithm, reader io.Reader) (*LabeledHash, error) {
hasher := alg.new()
defer hasher.Reset() // Internal state may leak data if kept in memory.
// Pipe all data directly to the hashing algorithm.
_, err := bufio.NewReader(reader).WriteTo(hasher)
if err != nil {
return nil, fmt.Errorf("failed to read: %w", err)
}
return &LabeledHash{
alg: alg,
digest: hasher.Sum(nil),
}, nil
}
// Load loads a labeled hash from the given []byte slice. // Load loads a labeled hash from the given []byte slice.
func Load(labeledHash []byte) (*LabeledHash, error) { func Load(labeledHash []byte) (*LabeledHash, error) {
c := container.New(labeledHash) c := container.New(labeledHash)
@ -95,6 +126,16 @@ func FromBase58(base58Encoded string) (*LabeledHash, error) {
return Load(raw) return Load(raw)
} }
// Algorithm returns the algorithm of the labeled hash.
func (lh *LabeledHash) Algorithm() Algorithm {
return lh.alg
}
// Sum returns the raw calculated hash digest.
func (lh *LabeledHash) Sum() []byte {
return lh.digest
}
// Bytes return the []byte representation of the labeled hash. // Bytes return the []byte representation of the labeled hash.
func (lh *LabeledHash) Bytes() []byte { func (lh *LabeledHash) Bytes() []byte {
c := container.New() c := container.New()
@ -127,16 +168,45 @@ func (lh *LabeledHash) Equal(other *LabeledHash) bool {
subtle.ConstantTimeCompare(lh.digest, other.digest) == 1 subtle.ConstantTimeCompare(lh.digest, other.digest) == 1
} }
// MatchesString returns true if the digest of the given string matches the hash. // EqualRaw returns true if the given raw hash digest is equal.
func (lh *LabeledHash) MatchesString(s string) bool { // Equality is checked by comparing both the digest value only.
return lh.MatchesData([]byte(s)) // The caller must make sure the same algorithm is used.
func (lh *LabeledHash) EqualRaw(otherDigest []byte) bool {
return subtle.ConstantTimeCompare(lh.digest, otherDigest) == 1
}
// Matches returns true if the digest of the given data matches the hash.
func (lh *LabeledHash) Matches(data []byte) bool {
return lh.Equal(Digest(lh.alg, data))
} }
// MatchesData returns true if the digest of the given data matches the hash. // MatchesData returns true if the digest of the given data matches the hash.
// Deprecated: Use Matches instead.
func (lh *LabeledHash) MatchesData(data []byte) bool { func (lh *LabeledHash) MatchesData(data []byte) bool {
hasher := lh.alg.new() return lh.Equal(Digest(lh.alg, data))
_, _ = hasher.Write(data) // never returns an error }
defer hasher.Reset() // internal state may leak data if kept in memory
// MatchesString returns true if the digest of the given string matches the hash.
return subtle.ConstantTimeCompare(lh.digest, hasher.Sum(nil)) == 1 func (lh *LabeledHash) MatchesString(s string) bool {
return lh.Matches([]byte(s))
}
// MatchesFile returns true if the digest of the given file matches the hash.
func (lh *LabeledHash) MatchesFile(pathToFile string) (bool, error) {
fileHash, err := DigestFile(lh.alg, pathToFile)
if err != nil {
return false, err
}
return lh.Equal(fileHash), nil
}
// MatchesReader returns true if the digest of the given reader matches the hash.
func (lh *LabeledHash) MatchesReader(reader io.Reader) (bool, error) {
readerHash, err := DigestFromReader(lh.alg, reader)
if err != nil {
return false, err
}
return lh.Equal(readerHash), nil
} }

View file

@ -42,13 +42,13 @@ func testAlgorithm(t *testing.T, alg Algorithm, emptyHex, foxHex string) {
} }
// test matching with serialized/loaded labeled hash // test matching with serialized/loaded labeled hash
if !lh.MatchesData(testFoxData) { if !lh.Matches(testFoxData) {
t.Errorf("alg %d: failed to match reference", alg) t.Errorf("alg %d: failed to match reference", alg)
} }
if !lh.MatchesString(testFox) { if !lh.MatchesString(testFox) {
t.Errorf("alg %d: failed to match reference", alg) t.Errorf("alg %d: failed to match reference", alg)
} }
if lh.MatchesData(noMatchData) { if lh.Matches(noMatchData) {
t.Errorf("alg %d: failed to non-match garbage", alg) t.Errorf("alg %d: failed to non-match garbage", alg)
} }
if lh.MatchesString(noMatch) { if lh.MatchesString(noMatch) {
@ -99,13 +99,13 @@ func testFormat(t *testing.T, alg Algorithm, lhs, loaded *LabeledHash) {
} }
// Test matching. // Test matching.
if !loaded.MatchesData(testFoxData) { if !loaded.Matches(testFoxData) {
t.Errorf("alg %d: failed to match reference", alg) t.Errorf("alg %d: failed to match reference", alg)
} }
if !loaded.MatchesString(testFox) { if !loaded.MatchesString(testFox) {
t.Errorf("alg %d: failed to match reference", alg) t.Errorf("alg %d: failed to match reference", alg)
} }
if loaded.MatchesData(noMatchData) { if loaded.Matches(noMatchData) {
t.Errorf("alg %d: failed to non-match garbage", alg) t.Errorf("alg %d: failed to non-match garbage", alg)
} }
if loaded.MatchesString(noMatch) { if loaded.MatchesString(noMatch) {

6
pack
View file

@ -70,12 +70,18 @@ function check_all {
GOOS=linux GOARCH=amd64 check GOOS=linux GOARCH=amd64 check
GOOS=windows GOARCH=amd64 check GOOS=windows GOARCH=amd64 check
GOOS=darwin GOARCH=amd64 check GOOS=darwin GOARCH=amd64 check
GOOS=linux GOARCH=arm64 check
GOOS=windows GOARCH=arm64 check
GOOS=darwin GOARCH=arm64 check
} }
function build_all { function build_all {
GOOS=linux GOARCH=amd64 build GOOS=linux GOARCH=amd64 build
GOOS=windows GOARCH=amd64 build GOOS=windows GOARCH=amd64 build
GOOS=darwin GOARCH=amd64 build GOOS=darwin GOARCH=amd64 build
GOOS=linux GOARCH=arm64 build
GOOS=windows GOARCH=arm64 build
GOOS=darwin GOARCH=arm64 build
} }
function build_os { function build_os {

View file

@ -17,7 +17,10 @@ const (
) )
var ( var (
wireReKeyAfterMsgs uint64 = 100000 // re-exchange keys every 100000 messages // Re-exchange keys every x messages.
// At 10_000_000 msgs with 1500 bytes per msg, this would result in
// re-exchanging keys every 15 GB.
wireReKeyAfterMsgs uint64 = 10_000_000
requiredWireSessionRequirements = NewRequirements().Remove(SenderAuthentication) requiredWireSessionRequirements = NewRequirements().Remove(SenderAuthentication)
) )

View file

@ -164,6 +164,7 @@ func newSession(e *Envelope) (*Session, error) { //nolint:maintidx
case tools.PurposeSigning: case tools.PurposeSigning:
s.signers = append(s.signers, logic) s.signers = append(s.signers, logic)
s.toolRequirements.Add(Integrity)
s.toolRequirements.Add(SenderAuthentication) s.toolRequirements.Add(SenderAuthentication)
case tools.PurposeIntegratedCipher: case tools.PurposeIntegratedCipher:

View file

@ -7,9 +7,11 @@ import (
"io" "io"
"time" "time"
"github.com/mr-tron/base58"
uuid "github.com/satori/go.uuid" uuid "github.com/satori/go.uuid"
"github.com/safing/jess/tools" "github.com/safing/jess/tools"
"github.com/safing/portbase/formats/dsd"
) )
// Special signet types. // Special signet types.
@ -134,6 +136,14 @@ func (signet *Signet) SetLoadedKeys(pubKey crypto.PublicKey, privKey crypto.Priv
// AsRecipient returns a public version of the Signet. // AsRecipient returns a public version of the Signet.
func (signet *Signet) AsRecipient() (*Signet, error) { func (signet *Signet) AsRecipient() (*Signet, error) {
// Check special signet schemes.
switch signet.Scheme {
case SignetSchemeKey:
return nil, errors.New("keys cannot be a recipient")
case SignetSchemePassword:
return nil, errors.New("passwords cannot be a recipient")
}
// load so we can split keys // load so we can split keys
err := signet.LoadKey() err := signet.LoadKey()
if err != nil { if err != nil {
@ -249,3 +259,60 @@ func (signet *Signet) AssignUUID() error {
signet.ID = u.String() signet.ID = u.String()
return nil return nil
} }
// ToBytes serializes the Signet to a byte slice.
func (signet *Signet) ToBytes() ([]byte, error) {
// Make sure the key is stored in the serializable format.
if err := signet.StoreKey(); err != nil {
return nil, fmt.Errorf("failed to serialize the key: %w", err)
}
// Serialize Signet.
data, err := dsd.Dump(signet, dsd.CBOR)
if err != nil {
return nil, fmt.Errorf("failed to serialize the signet: %w", err)
}
return data, nil
}
// SignetFromBytes parses and loads a serialized signet.
func SignetFromBytes(data []byte) (*Signet, error) {
signet := &Signet{}
// Parse Signet from data.
if _, err := dsd.Load(data, signet); err != nil {
return nil, fmt.Errorf("failed to parse data format: %w", err)
}
// Load the key.
if err := signet.LoadKey(); err != nil {
return nil, fmt.Errorf("failed to parse key: %w", err)
}
return signet, nil
}
// ToBase58 serializes the signet and encodes it with base58.
func (signet *Signet) ToBase58() (string, error) {
// Serialize Signet.
data, err := signet.ToBytes()
if err != nil {
return "", err
}
// Encode and return.
return base58.Encode(data), nil
}
// SignetFromBase58 parses and loads a base58 encoded serialized signet.
func SignetFromBase58(base58Encoded string) (*Signet, error) {
// Decode string.
data, err := base58.Decode(base58Encoded)
if err != nil {
return nil, fmt.Errorf("failed to decode base58: %w", err)
}
// Parse and return.
return SignetFromBytes(data)
}

View file

@ -35,7 +35,16 @@ var (
SuiteSignV1 = registerSuite(&Suite{ SuiteSignV1 = registerSuite(&Suite{
ID: "sign_v1", ID: "sign_v1",
Tools: []string{"Ed25519(BLAKE2b-256)"}, Tools: []string{"Ed25519(BLAKE2b-256)"},
Provides: newEmptyRequirements().Add(SenderAuthentication), Provides: newEmptyRequirements().Add(Integrity).Add(SenderAuthentication),
SecurityLevel: 128,
Status: SuiteStatusRecommended,
})
// SuiteSignFileV1 is a cipher suite for signing files (no encryption).
// SHA2_256 is chosen for better compatibility with other tool sets and workflows.
SuiteSignFileV1 = registerSuite(&Suite{
ID: "signfile_v1",
Tools: []string{"Ed25519(SHA2-256)"},
Provides: newEmptyRequirements().Add(Integrity).Add(SenderAuthentication),
SecurityLevel: 128, SecurityLevel: 128,
Status: SuiteStatusRecommended, Status: SuiteStatusRecommended,
}) })
@ -66,6 +75,8 @@ var (
SuiteRcptOnly = SuiteRcptOnlyV1 SuiteRcptOnly = SuiteRcptOnlyV1
// SuiteSign is a a cipher suite for signing (no encryption). // SuiteSign is a a cipher suite for signing (no encryption).
SuiteSign = SuiteSignV1 SuiteSign = SuiteSignV1
// SuiteSignFile is a a cipher suite for signing files (no encryption).
SuiteSignFile = SuiteSignFileV1
// SuiteComplete is a a cipher suite for both encrypting for someone and signing. // SuiteComplete is a a cipher suite for both encrypting for someone and signing.
SuiteComplete = SuiteCompleteV1 SuiteComplete = SuiteCompleteV1
// SuiteWire is a a cipher suite for network communication, including authentication of the server, but not the client. // SuiteWire is a a cipher suite for network communication, including authentication of the server, but not the client.

View file

@ -193,6 +193,7 @@ func suiteBullshitCheck(suite *Suite) error { //nolint:maintidx
case tools.PurposeSigning: case tools.PurposeSigning:
s.signers = append(s.signers, logic) s.signers = append(s.signers, logic)
s.toolRequirements.Add(Integrity)
s.toolRequirements.Add(SenderAuthentication) s.toolRequirements.Add(SenderAuthentication)
case tools.PurposeIntegratedCipher: case tools.PurposeIntegratedCipher:
@ -417,6 +418,7 @@ func computeSuiteAttributes(toolIDs []string, assumeKey bool) *Suite {
newSuite.Provides.Add(RecipientAuthentication) newSuite.Provides.Add(RecipientAuthentication)
case tools.PurposeSigning: case tools.PurposeSigning:
newSuite.Provides.Add(Integrity)
newSuite.Provides.Add(SenderAuthentication) newSuite.Provides.Add(SenderAuthentication)
case tools.PurposeIntegratedCipher: case tools.PurposeIntegratedCipher:

View file

@ -2,7 +2,6 @@ package jess
import ( import (
"github.com/safing/jess/tools" "github.com/safing/jess/tools"
// Import all tools. // Import all tools.
_ "github.com/safing/jess/tools/all" _ "github.com/safing/jess/tools/all"
) )

View file

@ -1,6 +1,4 @@
// Package all imports all tool subpackages // Package all imports all tool subpackages
//
//nolint:gci
package all package all
import ( import (

View file

@ -3,7 +3,7 @@ package gostdlib
import ( import (
"errors" "errors"
"golang.org/x/crypto/poly1305" //nolint:staticcheck,gci "golang.org/x/crypto/poly1305" //nolint:staticcheck // TODO: replace with newer package
"github.com/safing/jess/tools" "github.com/safing/jess/tools"
) )

View file

@ -1,9 +1,15 @@
package truststores package truststores
import ( import (
"errors"
"github.com/safing/jess" "github.com/safing/jess"
) )
// ErrNotSupportedByTrustStore is returned by trust stores if they do not
// support certain actions.
var ErrNotSupportedByTrustStore = errors.New("action not supported by trust store")
// ExtendedTrustStore holds a set of trusted Signets, Recipients and Envelopes. // ExtendedTrustStore holds a set of trusted Signets, Recipients and Envelopes.
type ExtendedTrustStore interface { type ExtendedTrustStore interface {
jess.TrustStore jess.TrustStore

View file

@ -2,7 +2,6 @@ package truststores
import ( import (
"errors" "errors"
"io/ioutil"
"os" "os"
"github.com/safing/jess" "github.com/safing/jess"
@ -27,7 +26,7 @@ func WriteSignetToFile(signet *jess.Signet, filename string) error {
} }
// write // write
err = ioutil.WriteFile(filename, data, 0o0600) err = os.WriteFile(filename, data, 0o0600)
if err != nil { if err != nil {
return err return err
} }
@ -37,7 +36,7 @@ func WriteSignetToFile(signet *jess.Signet, filename string) error {
// LoadSignetFromFile loads a signet from the given filepath. // LoadSignetFromFile loads a signet from the given filepath.
func LoadSignetFromFile(filename string) (*jess.Signet, error) { func LoadSignetFromFile(filename string) (*jess.Signet, error) {
data, err := ioutil.ReadFile(filename) data, err := os.ReadFile(filename)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return nil, jess.ErrSignetNotFound return nil, jess.ErrSignetNotFound
@ -72,7 +71,7 @@ func WriteEnvelopeToFile(envelope *jess.Envelope, filename string) error {
} }
// write to storage // write to storage
err = ioutil.WriteFile(filename, data, 0600) //nolint:gofumpt // gofumpt is ignorant of octal numbers. err = os.WriteFile(filename, data, 0600) //nolint:gofumpt // gofumpt is ignorant of octal numbers.
if err != nil { if err != nil {
return err return err
} }
@ -82,7 +81,7 @@ func WriteEnvelopeToFile(envelope *jess.Envelope, filename string) error {
// LoadEnvelopeFromFile loads an envelope from the given filepath. // LoadEnvelopeFromFile loads an envelope from the given filepath.
func LoadEnvelopeFromFile(filename string) (*jess.Envelope, error) { func LoadEnvelopeFromFile(filename string) (*jess.Envelope, error) {
data, err := ioutil.ReadFile(filename) data, err := os.ReadFile(filename)
if err != nil { if err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
return nil, jess.ErrEnvelopeNotFound return nil, jess.ErrEnvelopeNotFound

148
truststores/keyring.go Normal file
View file

@ -0,0 +1,148 @@
package truststores
import (
"errors"
"fmt"
"github.com/zalando/go-keyring"
"github.com/safing/jess"
)
const (
keyringServiceNamePrefix = "jess:"
keyringSelfcheckKey = "_selfcheck"
keyringSelfcheckValue = "!selfcheck"
)
// KeyringTrustStore is a trust store that uses the system keyring.
// It does not support listing entries, so it cannot be easily managed.
type KeyringTrustStore struct {
serviceName string
}
// NewKeyringTrustStore returns a new keyring trust store with the given service name.
// The effect of the service name depends on the operating system.
// Read more at https://pkg.go.dev/github.com/zalando/go-keyring
func NewKeyringTrustStore(serviceName string) (*KeyringTrustStore, error) {
krts := &KeyringTrustStore{
serviceName: keyringServiceNamePrefix + serviceName,
}
// Run a self-check.
err := keyring.Set(krts.serviceName, keyringSelfcheckKey, keyringSelfcheckValue)
if err != nil {
return nil, err
}
selfcheckReturn, err := keyring.Get(krts.serviceName, keyringSelfcheckKey)
if err != nil {
return nil, err
}
if selfcheckReturn != keyringSelfcheckValue {
return nil, errors.New("keyring is faulty")
}
return krts, nil
}
// GetSignet returns the Signet with the given ID.
func (krts *KeyringTrustStore) GetSignet(id string, recipient bool) (*jess.Signet, error) {
// Build ID.
if recipient {
id += recipientSuffix
} else {
id += signetSuffix
}
// Get data from keyring.
data, err := keyring.Get(krts.serviceName, id)
if err != nil {
return nil, fmt.Errorf("%w: %s", jess.ErrSignetNotFound, err)
}
// Parse and return.
return jess.SignetFromBase58(data)
}
// StoreSignet stores a Signet.
func (krts *KeyringTrustStore) StoreSignet(signet *jess.Signet) error {
// Build ID.
var id string
if signet.Public {
id = signet.ID + recipientSuffix
} else {
id = signet.ID + signetSuffix
}
// Serialize.
data, err := signet.ToBase58()
if err != nil {
return err
}
// Save to keyring.
return keyring.Set(krts.serviceName, id, data)
}
// DeleteSignet deletes the Signet or Recipient with the given ID.
func (krts *KeyringTrustStore) DeleteSignet(id string, recipient bool) error {
// Build ID.
if recipient {
id += recipientSuffix
} else {
id += signetSuffix
}
// Delete from keyring.
return keyring.Delete(krts.serviceName, id)
}
// SelectSignets returns a selection of the signets in the trust store. Results are filtered by tool/algorithm and whether it you're looking for a signet (private key) or a recipient (public key).
func (krts *KeyringTrustStore) SelectSignets(filter uint8, schemes ...string) ([]*jess.Signet, error) {
return nil, ErrNotSupportedByTrustStore
}
// GetEnvelope returns the Envelope with the given name.
func (krts *KeyringTrustStore) GetEnvelope(name string) (*jess.Envelope, error) {
// Build ID.
name += envelopeSuffix
// Get data from keyring.
data, err := keyring.Get(krts.serviceName, name)
if err != nil {
return nil, fmt.Errorf("%w: %s", jess.ErrEnvelopeNotFound, err)
}
// Parse and return.
return jess.EnvelopeFromBase58(data)
}
// StoreEnvelope stores an Envelope.
func (krts *KeyringTrustStore) StoreEnvelope(envelope *jess.Envelope) error {
// Build ID.
name := envelope.Name + envelopeSuffix
// Serialize.
data, err := envelope.ToBase58()
if err != nil {
return err
}
// Save to keyring.
return keyring.Set(krts.serviceName, name, data)
}
// DeleteEnvelope deletes the Envelope with the given name.
func (krts *KeyringTrustStore) DeleteEnvelope(name string) error {
// Build ID.
name += envelopeSuffix
// Delete from keyring.
return keyring.Delete(krts.serviceName, name)
}
// AllEnvelopes returns all envelopes.
func (krts *KeyringTrustStore) AllEnvelopes() ([]*jess.Envelope, error) {
return nil, ErrNotSupportedByTrustStore
}