Merge pull request #80 from safing/feature/file-sigs
Add file signature support
This commit is contained in:
commit
a63e42986a
53 changed files with 2225 additions and 137 deletions
.github/workflows
.gitignore.golangci.ymlcmd
buildcfg-envelope.gocfg-signet.gocmd-close.gocmd-generate.gocmd-import-export.gocmd-open.gocmd-sign.gocmd-verify.goformat_sig.gomain.go
core_test.goenvelope.gotestdata
filesig
go.modgo.sumhashtools
import_export.golhash
packsession-wire.gosession.gosignet.gosuites.gosuites_test.gotools.gotools
truststores
8
.github/workflows/go.yml
vendored
8
.github/workflows/go.yml
vendored
|
@ -20,14 +20,14 @@ jobs:
|
|||
|
||||
- uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '^1.18'
|
||||
go-version: '^1.19'
|
||||
|
||||
- name: Run golangci-lint
|
||||
uses: golangci/golangci-lint-action@v3
|
||||
with:
|
||||
version: v1.45.1
|
||||
version: v1.49.0
|
||||
only-new-issues: true
|
||||
args: -c ./.golangci.yml
|
||||
args: -c ./.golangci.yml --timeout 15m
|
||||
|
||||
- name: Get dependencies
|
||||
run: go mod download
|
||||
|
@ -45,7 +45,7 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '^1.18'
|
||||
go-version: '^1.19'
|
||||
|
||||
- name: Get dependencies
|
||||
run: go mod download
|
||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,8 +1,7 @@
|
|||
cpu.out
|
||||
vendor
|
||||
cmd/cmd*
|
||||
cmd/jess*
|
||||
dist
|
||||
|
||||
# Custom dev deops
|
||||
# Custom dev deps
|
||||
go.mod.*
|
||||
|
|
|
@ -7,8 +7,8 @@ linters:
|
|||
- containedctx
|
||||
- contextcheck
|
||||
- cyclop
|
||||
- errchkjson # Triggers stack overflow on github.com/safing/jess
|
||||
- exhaustivestruct
|
||||
- exhaustruct
|
||||
- forbidigo
|
||||
- funlen
|
||||
- gochecknoglobals
|
||||
|
@ -18,6 +18,7 @@ linters:
|
|||
- goerr113
|
||||
- gomnd
|
||||
- ifshort
|
||||
- interfacebloat
|
||||
- interfacer
|
||||
- ireturn
|
||||
- lll
|
||||
|
@ -25,6 +26,9 @@ linters:
|
|||
- nilnil
|
||||
- nlreturn
|
||||
- noctx
|
||||
- nolintlint
|
||||
- nonamedreturns
|
||||
- nosnakecase
|
||||
- revive
|
||||
- tagliatelle
|
||||
- testpackage
|
||||
|
|
10
cmd/build
10
cmd/build
|
@ -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 "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_PATH="github.com/safing/jess/vendor/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}" $@
|
||||
BUILD_PATH="github.com/safing/portbase/info"
|
||||
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}" "$@"
|
||||
|
|
|
@ -30,7 +30,8 @@ func newEnvelope(name string) (*jess.Envelope, error) {
|
|||
"Encrypt with key",
|
||||
"Encrypt for someone and 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)
|
||||
|
@ -54,9 +55,12 @@ func newEnvelope(name string) (*jess.Envelope, error) {
|
|||
case "Encrypt for someone but don't sign":
|
||||
envelope.SuiteID = jess.SuiteRcptOnly
|
||||
err = selectSignets(envelope, "recipient")
|
||||
case "Sign a file":
|
||||
case "Sign a file (wrapped)":
|
||||
envelope.SuiteID = jess.SuiteSign
|
||||
err = selectSignets(envelope, "sender")
|
||||
case "Sign a file (separate sig)":
|
||||
envelope.SuiteID = jess.SuiteSignFile
|
||||
err = selectSignets(envelope, "sender")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -93,6 +97,7 @@ func editEnvelope(envelope *jess.Envelope) error {
|
|||
{"Recipients", formatSignetNames(envelope.Recipients)},
|
||||
{"Senders", formatSignetNames(envelope.Senders)},
|
||||
{""},
|
||||
{"Export", "export to text format"},
|
||||
{"Abort", "discard changes and return"},
|
||||
{"Delete", "delete and return"},
|
||||
}),
|
||||
|
@ -105,8 +110,28 @@ func editEnvelope(envelope *jess.Envelope) error {
|
|||
|
||||
switch {
|
||||
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)
|
||||
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"):
|
||||
return nil
|
||||
case strings.HasPrefix(submenu, "Delete"):
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
)
|
||||
|
||||
//nolint:gocognit
|
||||
func newSignet(name, scheme string) (*jess.Signet, error) {
|
||||
func newSignet(name, scheme string, saveToTrustStore bool) (*jess.Signet, error) {
|
||||
// get name
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
|
@ -110,28 +110,30 @@ func newSignet(name, scheme string) (*jess.Signet, error) {
|
|||
Created: time.Now(),
|
||||
}
|
||||
|
||||
// write signet
|
||||
err = trustStore.StoreSignet(signet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if saveToTrustStore {
|
||||
// write signet
|
||||
err = trustStore.StoreSignet(signet)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// export as recipient
|
||||
switch scheme {
|
||||
case jess.SignetSchemePassword, jess.SignetSchemeKey:
|
||||
// is secret, no recipient
|
||||
default:
|
||||
rcpt, err := signet.AsRecipient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = rcpt.StoreKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = trustStore.StoreSignet(rcpt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
// export as recipient
|
||||
switch scheme {
|
||||
case jess.SignetSchemePassword, jess.SignetSchemeKey:
|
||||
// is secret, no recipient
|
||||
default:
|
||||
rcpt, err := signet.AsRecipient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = rcpt.StoreKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = trustStore.StoreSignet(rcpt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ package main
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
func init() {
|
||||
rootCmd.AddCommand(closeCmd)
|
||||
closeCmd.Flags().StringVarP(&closeFlagOutput, "output", "o", "", "specify output file (`-` for stdout")
|
||||
closeCmd.Flags().StringVarP(&closeFlagOutput, "output", "o", "", "specify output file (`-` for stdout)")
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -49,10 +49,10 @@ var (
|
|||
filename := args[0]
|
||||
outputFilename := closeFlagOutput
|
||||
if outputFilename == "" {
|
||||
if strings.HasSuffix(filename, ".letter") {
|
||||
if strings.HasSuffix(filename, letterFileExtension) {
|
||||
return errors.New("cannot automatically derive output filename, please specify with --output")
|
||||
}
|
||||
outputFilename = filename + ".letter"
|
||||
outputFilename = filename + letterFileExtension
|
||||
}
|
||||
// check input file
|
||||
if filename != "-" {
|
||||
|
@ -89,9 +89,9 @@ var (
|
|||
// load file
|
||||
var data []byte
|
||||
if filename == "-" {
|
||||
data, err = ioutil.ReadAll(os.Stdin)
|
||||
data, err = io.ReadAll(os.Stdin)
|
||||
} else {
|
||||
data, err = ioutil.ReadFile(filename)
|
||||
data, err = os.ReadFile(filename)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
|
@ -8,11 +10,13 @@ func init() {
|
|||
rootCmd.AddCommand(generateCmd)
|
||||
generateCmd.Flags().StringVarP(&generateFlagName, "name", "l", "", "specify signet name/label")
|
||||
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 (
|
||||
generateFlagName string
|
||||
generateFlagScheme string
|
||||
generateFlagName string
|
||||
generateFlagScheme string
|
||||
generateFlagTextOnly bool
|
||||
|
||||
generateCmd = &cobra.Command{
|
||||
Use: "generate",
|
||||
|
@ -21,8 +25,43 @@ var (
|
|||
Args: cobra.NoArgs,
|
||||
PreRunE: requireTrustStore,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
_, err := newSignet(generateFlagName, generateFlagScheme)
|
||||
return err
|
||||
// Generate new signet
|
||||
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
170
cmd/cmd-import-export.go
Normal 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
|
||||
}
|
|
@ -4,7 +4,6 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
|
@ -79,9 +78,9 @@ var (
|
|||
// load file
|
||||
var data []byte
|
||||
if filename == "-" {
|
||||
data, err = ioutil.ReadAll(os.Stdin)
|
||||
data, err = io.ReadAll(os.Stdin)
|
||||
} else {
|
||||
data, err = ioutil.ReadFile(filename)
|
||||
data, err = os.ReadFile(filename)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -93,6 +92,11 @@ var (
|
|||
return err
|
||||
}
|
||||
|
||||
// Create default requirements if not set.
|
||||
if requirements == nil {
|
||||
requirements = jess.NewRequirements()
|
||||
}
|
||||
|
||||
// decrypt (and verify)
|
||||
plainText, err := letter.Open(requirements, trustStore)
|
||||
if err != nil {
|
||||
|
|
62
cmd/cmd-sign.go
Normal file
62
cmd/cmd-sign.go
Normal 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
|
||||
},
|
||||
}
|
||||
)
|
|
@ -3,85 +3,260 @@ package main
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/safing/jess"
|
||||
"github.com/safing/jess/filesig"
|
||||
"github.com/safing/portbase/container"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(verifyCmd)
|
||||
verifyCmd.Flags().StringToStringVarP(&metaDataFlag, "metadata", "m", nil, "specify file metadata to verify (.sig only)")
|
||||
}
|
||||
|
||||
var (
|
||||
verifyCmdHelp = "usage: jess verify <file>"
|
||||
var verifyCmd = &cobra.Command{
|
||||
Use: "verify <files and directories>",
|
||||
Short: "verify signed files and files in directories",
|
||||
DisableFlagsInUseLine: true,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PreRunE: requireTrustStore,
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
var verificationFails, verificationWarnings int
|
||||
|
||||
verifyCmd = &cobra.Command{
|
||||
Use: "verify <file>",
|
||||
Short: "verify file",
|
||||
DisableFlagsInUseLine: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) (err error) {
|
||||
// check args
|
||||
if len(args) != 1 {
|
||||
return errors.New(verifyCmdHelp)
|
||||
// Check if we are only verifying a single file.
|
||||
if len(args) == 1 {
|
||||
matches, err := filepath.Glob(args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check filenames
|
||||
filename := args[0]
|
||||
// check input file
|
||||
if filename != "-" {
|
||||
fileInfo, err := os.Stat(filename)
|
||||
switch len(matches) {
|
||||
case 0:
|
||||
return errors.New("file not found")
|
||||
case 1:
|
||||
// Check if the single match is a file.
|
||||
fileInfo, err := os.Stat(matches[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if fileInfo.Size() > warnFileSize {
|
||||
confirmed, err := confirm("Input file is really big (%s) and jess needs to load it fully to memory, continue?", true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !confirmed {
|
||||
return nil
|
||||
}
|
||||
// Verify file if it is not a directory.
|
||||
if !fileInfo.IsDir() {
|
||||
return verify(matches[0], false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// load file
|
||||
var data []byte
|
||||
if filename == "-" {
|
||||
data, err = ioutil.ReadAll(os.Stdin)
|
||||
} else {
|
||||
data, err = ioutil.ReadFile(filename)
|
||||
}
|
||||
// Resolve globs.
|
||||
files := make([]string, 0, len(args))
|
||||
for _, arg := range args {
|
||||
matches, err := filepath.Glob(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files = append(files, matches...)
|
||||
}
|
||||
|
||||
// parse file
|
||||
letter, err := jess.LetterFromFileFormat(container.New(data))
|
||||
// Go through all files.
|
||||
for _, file := range files {
|
||||
fileInfo, err := os.Stat(file)
|
||||
if err != nil {
|
||||
return err
|
||||
verificationWarnings++
|
||||
fmt.Printf("[WARN] %s failed to read: %s\n", file, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// adjust requirements
|
||||
if requirements == nil {
|
||||
requirements = jess.NewRequirements().
|
||||
Remove(jess.Confidentiality).
|
||||
Remove(jess.Integrity).
|
||||
Remove(jess.RecipientAuthentication)
|
||||
// Walk directories.
|
||||
if fileInfo.IsDir() {
|
||||
err := filepath.Walk(file, func(path string, info fs.FileInfo, err error) error {
|
||||
// Log walking errors.
|
||||
if err != nil {
|
||||
verificationWarnings++
|
||||
fmt.Printf("[WARN] %s failed to read: %s\n", path, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only verify if .sig or .letter.
|
||||
if strings.HasSuffix(path, 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
|
||||
err = letter.Verify(requirements, trustStore)
|
||||
if err != nil {
|
||||
return err
|
||||
if err := verify(file, true); err != nil {
|
||||
verificationFails++
|
||||
}
|
||||
}
|
||||
|
||||
// success
|
||||
fmt.Println("ok")
|
||||
return nil
|
||||
},
|
||||
// End with error status if any verification failed.
|
||||
if verificationFails > 0 {
|
||||
return fmt.Errorf("%d verification failures", verificationFails)
|
||||
}
|
||||
if verificationWarnings > 0 {
|
||||
return fmt.Errorf("%d warnings", verificationWarnings)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var verifiedSigs = make(map[string]struct{})
|
||||
|
||||
func verify(filename string, bulkMode bool) error {
|
||||
// Check if file was already verified.
|
||||
if _, alreadyVerified := verifiedSigs[filename]; alreadyVerified {
|
||||
return nil
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
signame string
|
||||
signedBy []string
|
||||
err error
|
||||
)
|
||||
|
||||
// Get correct files and verify.
|
||||
switch {
|
||||
case filename == stdInOutFilename:
|
||||
signedBy, err = verifyLetter(filename, bulkMode)
|
||||
case strings.HasSuffix(filename, letterFileExtension):
|
||||
signedBy, err = verifyLetter(filename, bulkMode)
|
||||
case strings.HasSuffix(filename, 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
79
cmd/format_sig.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/safing/jess/filesig"
|
||||
)
|
||||
|
||||
func formatSignatures(filename, signame string, fds []*filesig.FileData) string {
|
||||
b := &strings.Builder{}
|
||||
|
||||
switch len(fds) {
|
||||
case 0:
|
||||
case 1:
|
||||
formatSignature(b, fds[0])
|
||||
case 2:
|
||||
for _, fd := range fds {
|
||||
fmt.Fprintf(b, "%d Signatures:\n\n\n", len(fds))
|
||||
formatSignature(b, fd)
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
if filename != "" || signame != "" {
|
||||
b.WriteString("\n")
|
||||
fmt.Fprintf(b, "File: %s\n", filename)
|
||||
fmt.Fprintf(b, "Sig: %s\n", signame)
|
||||
}
|
||||
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func formatSignature(b *strings.Builder, fd *filesig.FileData) {
|
||||
if fd.VerificationError() == nil {
|
||||
b.WriteString("Verification: OK\n")
|
||||
} else {
|
||||
fmt.Fprintf(b, "Verification FAILED: %s\n", fd.VerificationError())
|
||||
}
|
||||
|
||||
if letter := fd.Signature(); letter != nil {
|
||||
b.WriteString("\n")
|
||||
for _, sig := range letter.Signatures {
|
||||
signet, err := trustStore.GetSignet(sig.ID, true)
|
||||
if err == nil {
|
||||
fmt.Fprintf(b, "Signed By: %s (%s)\n", signet.Info.Name, sig.ID)
|
||||
} else {
|
||||
fmt.Fprintf(b, "Signed By: %s\n", sig.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if fileHash := fd.FileHash(); fileHash != nil {
|
||||
b.WriteString("\n")
|
||||
fmt.Fprintf(b, "Hash Alg: %s\n", fileHash.Algorithm())
|
||||
fmt.Fprintf(b, "Hash Sum: %s\n", hex.EncodeToString(fileHash.Sum()))
|
||||
}
|
||||
|
||||
if len(fd.MetaData) > 0 {
|
||||
b.WriteString("\nMetadata:\n")
|
||||
|
||||
sortedMetaData := make([][]string, 0, len(fd.MetaData))
|
||||
for k, v := range fd.MetaData {
|
||||
sortedMetaData = append(sortedMetaData, []string{k, v})
|
||||
}
|
||||
sort.Sort(sortByMetaDataKey(sortedMetaData))
|
||||
for _, entry := range sortedMetaData {
|
||||
fmt.Fprintf(b, " %s: %s\n", entry[0], entry[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type sortByMetaDataKey [][]string
|
||||
|
||||
func (a sortByMetaDataKey) Len() int { return len(a) }
|
||||
func (a sortByMetaDataKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
func (a sortByMetaDataKey) Less(i, j int) bool { return a[i][0] < a[j][0] }
|
35
cmd/main.go
35
cmd/main.go
|
@ -14,6 +14,9 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
stdInOutFilename = "-"
|
||||
letterFileExtension = ".letter"
|
||||
|
||||
warnFileSize = 12000000 // 120MB
|
||||
)
|
||||
|
||||
|
@ -28,12 +31,13 @@ var (
|
|||
}
|
||||
|
||||
trustStoreDir string
|
||||
trustStoreKeyring string
|
||||
noSpec string
|
||||
minimumSecurityLevel = 0
|
||||
defaultSymmetricKeySize = 0
|
||||
|
||||
trustStore truststores.ExtendedTrustStore
|
||||
requirements = jess.NewRequirements()
|
||||
requirements *jess.Requirements
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -46,7 +50,10 @@ func main() {
|
|||
}
|
||||
|
||||
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", "",
|
||||
"remove requirements using the abbreviations C, I, R, S",
|
||||
|
@ -63,18 +70,36 @@ func main() {
|
|||
}
|
||||
|
||||
func initGlobalFlags(cmd *cobra.Command, args []string) (err error) {
|
||||
// trust store
|
||||
// trust store directory
|
||||
if trustStoreDir == "" {
|
||||
trustStoreDir, _ = os.LookupEnv("JESS_TSDIR")
|
||||
trustStoreDir, _ = os.LookupEnv("JESS_TS_DIR")
|
||||
if trustStoreDir == "" {
|
||||
trustStoreDir, _ = os.LookupEnv("JESS_TSDIR")
|
||||
}
|
||||
}
|
||||
if trustStoreDir != "" {
|
||||
var err error
|
||||
trustStore, err = truststores.NewDirTrustStore(trustStoreDir)
|
||||
if err != nil {
|
||||
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
|
||||
if noSpec != "" {
|
||||
requirements, err = jess.ParseRequirementsFromNoSpec(noSpec)
|
||||
|
|
14
cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.recipient
vendored
Normal file
14
cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.recipient
vendored
Normal 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
|
||||
}
|
||||
}
|
13
cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.signet
vendored
Normal file
13
cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.signet
vendored
Normal 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
|
||||
}
|
||||
}
|
23
cmd/testdata/.truststore/safing-codesign-1.envelope
vendored
Normal file
23
cmd/testdata/.truststore/safing-codesign-1.envelope
vendored
Normal 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
1
cmd/testdata/test.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
hello world!
|
13
cmd/testdata/test.txt.letter
vendored
Normal file
13
cmd/testdata/test.txt.letter
vendored
Normal 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
9
cmd/testdata/test.txt.sig
vendored
Normal 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
1
cmd/testdata/test3.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
hello world!!
|
9
cmd/testdata/test3.txt.sig
vendored
Normal file
9
cmd/testdata/test3.txt.sig
vendored
Normal 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
1
cmd/testdata/test4.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
hello world!
|
1
cmd/testdata/testdir/test2.txt
vendored
Normal file
1
cmd/testdata/testdir/test2.txt
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
hello world!
|
9
cmd/testdata/testdir/test2.txt.sig
vendored
Normal file
9
cmd/testdata/testdir/test2.txt.sig
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
-----BEGIN JESS SIGNATURE-----
|
||||
Q6VnVmVyc2lvbgFnU3VpdGVJRGtzaWduZmlsZV92MWVOb25jZUThzxO6ZERhdGFY
|
||||
d02Dq0xhYmVsZWRIYXNoxCIJIOz3Afcn2eLXfEqkmsb7vMmXJ4rKAQvd7rlhwQz1
|
||||
TUNaqFNpZ25lZEF01v9izC3SqE1ldGFEYXRhgqd2ZXJzaW9upTAuMC4xqmlkZW50
|
||||
aWZpZXKyd2luZG93cy9jb2RlL3RoaW5nalNpZ25hdHVyZXOBo2ZTY2hlbWVnRWQy
|
||||
NTUxOWJJRHgkMzkxMWM4NGMtNzhmNy00MzU0LWE3ZjUtMGUxMTVhYTI5MDNjZVZh
|
||||
bHVlWEAGLkIoej0+ilJrIyb+BzX8+Yw2LY0zkoL9vwI02/2KqKVT7/pH+LTDX1Hl
|
||||
h1epYkF8ICdwa1iVNDx6P7iNmWkL
|
||||
-----END JESS SIGNATURE-----
|
18
core_test.go
18
core_test.go
|
@ -225,10 +225,8 @@ func TestCoreAllCombinations(t *testing.T) {
|
|||
t.Logf("of these, %d were successfully detected as invalid", combinationsDetectedInvalid)
|
||||
}
|
||||
|
||||
func testStorage(t *testing.T, suite *Suite) (detectedInvalid bool) {
|
||||
t.Helper()
|
||||
|
||||
// t.Logf("testing storage with %s", suite.ID)
|
||||
func testStorage(t *testing.T, suite *Suite) (detectedInvalid bool) { //nolint:thelper
|
||||
t.Logf("testing storage with %s", suite.ID)
|
||||
|
||||
e, err := setupEnvelopeAndTrustStore(t, suite)
|
||||
if err != nil {
|
||||
|
@ -378,6 +376,7 @@ func setupEnvelopeAndTrustStore(t *testing.T, suite *Suite) (*Envelope, error) {
|
|||
case tools.PurposeKeyEncapsulation:
|
||||
e.suite.Provides.Add(RecipientAuthentication)
|
||||
case tools.PurposeSigning:
|
||||
e.suite.Provides.Add(Integrity)
|
||||
e.suite.Provides.Add(SenderAuthentication)
|
||||
case tools.PurposeIntegratedCipher:
|
||||
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
|
||||
if !keyDerPresent &&
|
||||
(len(e.suite.Provides.all) != 1 ||
|
||||
!e.suite.Provides.Has(SenderAuthentication)) {
|
||||
if !keyDerPresent && len(e.Senders) != len(e.suite.Tools) {
|
||||
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.
|
||||
// Forked from https://github.com/mxschmitt/golang-combinations/blob/a887187146560effd2677e987b069262f356297f/combinations.go
|
||||
// Copyright (c) 2018 Max Schmitt,
|
||||
// MIT License.
|
||||
//
|
||||
// Forked from https://github.com/mxschmitt/golang-combinations/blob/a887187146560effd2677e987b069262f356297f/combinations.go
|
||||
// Copyright (c) 2018 Max Schmitt,
|
||||
// MIT License.
|
||||
func generateCombinations(set []string) (subsets [][]string) {
|
||||
length := uint(len(set))
|
||||
|
||||
|
|
81
envelope.go
81
envelope.go
|
@ -3,6 +3,10 @@ package jess
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/mr-tron/base58"
|
||||
|
||||
"github.com/safing/portbase/formats/dsd"
|
||||
)
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
123
filesig/format_armor.go
Normal 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
|
||||
}
|
197
filesig/format_armor_test.go
Normal file
197
filesig/format_armor_test.go
Normal 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
146
filesig/helpers.go
Normal 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
123
filesig/main.go
Normal 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
130
filesig/main_test.go
Normal 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
1
go.mod
|
@ -12,6 +12,7 @@ require (
|
|||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/spf13/cobra v1.5.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/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
)
|
||||
|
|
8
go.sum
8
go.sum
|
@ -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/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/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/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=
|
||||
|
@ -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.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
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.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
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-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.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/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||
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.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
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.6/go.mod h1:qXsaaIqmgQH0T+OPdb99Bf+PKfBBQVAdyD6TY9G8XM4=
|
||||
go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
|
||||
|
|
|
@ -6,6 +6,8 @@ import (
|
|||
// Register BLAKE2 in Go's internal registry.
|
||||
_ "golang.org/x/crypto/blake2b"
|
||||
_ "golang.org/x/crypto/blake2s"
|
||||
|
||||
"github.com/safing/jess/lhash"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -21,6 +23,7 @@ func init() {
|
|||
BlockSize: crypto.BLAKE2s_256.New().BlockSize(),
|
||||
SecurityLevel: 128,
|
||||
Comment: "RFC 7693, successor of SHA3 finalist, optimized for 8-32 bit software",
|
||||
labeledAlg: lhash.BLAKE2s_256,
|
||||
}))
|
||||
Register(blake2bBase.With(&HashTool{
|
||||
Name: "BLAKE2b-256",
|
||||
|
@ -28,6 +31,7 @@ func init() {
|
|||
DigestSize: crypto.BLAKE2b_256.Size(),
|
||||
BlockSize: crypto.BLAKE2b_256.New().BlockSize(),
|
||||
SecurityLevel: 128,
|
||||
labeledAlg: lhash.BLAKE2b_256,
|
||||
}))
|
||||
Register(blake2bBase.With(&HashTool{
|
||||
Name: "BLAKE2b-384",
|
||||
|
@ -35,6 +39,7 @@ func init() {
|
|||
DigestSize: crypto.BLAKE2b_384.Size(),
|
||||
BlockSize: crypto.BLAKE2b_384.New().BlockSize(),
|
||||
SecurityLevel: 192,
|
||||
labeledAlg: lhash.BLAKE2b_384,
|
||||
}))
|
||||
Register(blake2bBase.With(&HashTool{
|
||||
Name: "BLAKE2b-512",
|
||||
|
@ -42,5 +47,6 @@ func init() {
|
|||
DigestSize: crypto.BLAKE2b_512.Size(),
|
||||
BlockSize: crypto.BLAKE2b_512.New().BlockSize(),
|
||||
SecurityLevel: 256,
|
||||
labeledAlg: lhash.BLAKE2b_512,
|
||||
}))
|
||||
}
|
||||
|
|
|
@ -3,6 +3,8 @@ package hashtools
|
|||
import (
|
||||
"crypto"
|
||||
"hash"
|
||||
|
||||
"github.com/safing/jess/lhash"
|
||||
)
|
||||
|
||||
// HashTool holds generic information about a hash tool.
|
||||
|
@ -16,6 +18,8 @@ type HashTool struct {
|
|||
|
||||
Comment string
|
||||
Author string
|
||||
|
||||
labeledAlg lhash.Algorithm
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
changes.Author = ht.Author
|
||||
}
|
||||
if changes.labeledAlg == 0 {
|
||||
changes.labeledAlg = ht.labeledAlg
|
||||
}
|
||||
|
||||
return changes
|
||||
}
|
||||
|
||||
// LabeledHasher returns the corresponding labeled hashing algorithm.
|
||||
func (ht *HashTool) LabeledHasher() lhash.Algorithm {
|
||||
return ht.labeledAlg
|
||||
}
|
||||
|
|
|
@ -2,13 +2,14 @@ package hashtools
|
|||
|
||||
import (
|
||||
"crypto"
|
||||
|
||||
// Register SHA2 in Go's internal registry.
|
||||
_ "crypto/sha256"
|
||||
_ "crypto/sha512"
|
||||
|
||||
// Register SHA3 in Go's internal registry.
|
||||
_ "golang.org/x/crypto/sha3"
|
||||
|
||||
"github.com/safing/jess/lhash"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -24,6 +25,7 @@ func init() {
|
|||
BlockSize: crypto.SHA224.New().BlockSize(),
|
||||
SecurityLevel: 112,
|
||||
Author: "NSA, 2004",
|
||||
labeledAlg: lhash.SHA2_224,
|
||||
}))
|
||||
Register(sha2Base.With(&HashTool{
|
||||
Name: "SHA2-256",
|
||||
|
@ -31,6 +33,7 @@ func init() {
|
|||
DigestSize: crypto.SHA256.Size(),
|
||||
BlockSize: crypto.SHA256.New().BlockSize(),
|
||||
SecurityLevel: 128,
|
||||
labeledAlg: lhash.SHA2_256,
|
||||
}))
|
||||
Register(sha2Base.With(&HashTool{
|
||||
Name: "SHA2-384",
|
||||
|
@ -38,6 +41,7 @@ func init() {
|
|||
DigestSize: crypto.SHA384.Size(),
|
||||
BlockSize: crypto.SHA384.New().BlockSize(),
|
||||
SecurityLevel: 192,
|
||||
labeledAlg: lhash.SHA2_384,
|
||||
}))
|
||||
Register(sha2Base.With(&HashTool{
|
||||
Name: "SHA2-512",
|
||||
|
@ -45,6 +49,7 @@ func init() {
|
|||
DigestSize: crypto.SHA512.Size(),
|
||||
BlockSize: crypto.SHA512.New().BlockSize(),
|
||||
SecurityLevel: 256,
|
||||
labeledAlg: lhash.SHA2_512,
|
||||
}))
|
||||
Register(sha2Base.With(&HashTool{
|
||||
Name: "SHA2-512-224",
|
||||
|
@ -52,6 +57,7 @@ func init() {
|
|||
DigestSize: crypto.SHA512_224.Size(),
|
||||
BlockSize: crypto.SHA512_224.New().BlockSize(),
|
||||
SecurityLevel: 112,
|
||||
labeledAlg: lhash.SHA2_512_224,
|
||||
}))
|
||||
Register(sha2Base.With(&HashTool{
|
||||
Name: "SHA2-512-256",
|
||||
|
@ -59,6 +65,7 @@ func init() {
|
|||
DigestSize: crypto.SHA512_256.Size(),
|
||||
BlockSize: crypto.SHA512_256.New().BlockSize(),
|
||||
SecurityLevel: 128,
|
||||
labeledAlg: lhash.SHA2_512_256,
|
||||
}))
|
||||
|
||||
// SHA3
|
||||
|
@ -72,6 +79,7 @@ func init() {
|
|||
DigestSize: crypto.SHA3_224.Size(),
|
||||
BlockSize: crypto.SHA3_224.New().BlockSize(),
|
||||
SecurityLevel: 112,
|
||||
labeledAlg: lhash.SHA3_224,
|
||||
}))
|
||||
Register(sha3Base.With(&HashTool{
|
||||
Name: "SHA3-256",
|
||||
|
@ -79,6 +87,7 @@ func init() {
|
|||
DigestSize: crypto.SHA3_256.Size(),
|
||||
BlockSize: crypto.SHA3_256.New().BlockSize(),
|
||||
SecurityLevel: 128,
|
||||
labeledAlg: lhash.SHA3_256,
|
||||
}))
|
||||
Register(sha3Base.With(&HashTool{
|
||||
Name: "SHA3-384",
|
||||
|
@ -86,6 +95,7 @@ func init() {
|
|||
DigestSize: crypto.SHA3_384.Size(),
|
||||
BlockSize: crypto.SHA3_384.New().BlockSize(),
|
||||
SecurityLevel: 192,
|
||||
labeledAlg: lhash.SHA3_384,
|
||||
}))
|
||||
Register(sha3Base.With(&HashTool{
|
||||
Name: "SHA3-512",
|
||||
|
@ -93,5 +103,6 @@ func init() {
|
|||
DigestSize: crypto.SHA3_512.Size(),
|
||||
BlockSize: crypto.SHA3_512.New().BlockSize(),
|
||||
SecurityLevel: 256,
|
||||
labeledAlg: lhash.SHA3_512,
|
||||
}))
|
||||
}
|
||||
|
|
195
import_export.go
Normal file
195
import_export.go
Normal 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, "-"), "-",
|
||||
),
|
||||
)
|
||||
}
|
|
@ -6,6 +6,7 @@ package lhash
|
|||
import (
|
||||
"crypto"
|
||||
"hash"
|
||||
"io"
|
||||
|
||||
// Register SHA2 in Go's internal registry.
|
||||
_ "crypto/sha256"
|
||||
|
@ -83,3 +84,65 @@ func (a Algorithm) new() hash.Hash {
|
|||
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)
|
||||
}
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
package lhash
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/mr-tron/base58"
|
||||
|
||||
|
@ -21,8 +24,8 @@ type LabeledHash struct {
|
|||
// Digest creates a new labeled hash and digests the given data.
|
||||
func Digest(alg Algorithm, data []byte) *LabeledHash {
|
||||
hasher := alg.new()
|
||||
_, _ = hasher.Write(data) // never returns an error
|
||||
defer hasher.Reset() // internal state may leak data if kept in memory
|
||||
_, _ = hasher.Write(data) // Never returns an error.
|
||||
defer hasher.Reset() // Internal state may leak data if kept in memory.
|
||||
|
||||
return &LabeledHash{
|
||||
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.
|
||||
func Load(labeledHash []byte) (*LabeledHash, error) {
|
||||
c := container.New(labeledHash)
|
||||
|
@ -95,6 +126,16 @@ func FromBase58(base58Encoded string) (*LabeledHash, error) {
|
|||
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.
|
||||
func (lh *LabeledHash) Bytes() []byte {
|
||||
c := container.New()
|
||||
|
@ -127,16 +168,45 @@ func (lh *LabeledHash) Equal(other *LabeledHash) bool {
|
|||
subtle.ConstantTimeCompare(lh.digest, other.digest) == 1
|
||||
}
|
||||
|
||||
// MatchesString returns true if the digest of the given string matches the hash.
|
||||
func (lh *LabeledHash) MatchesString(s string) bool {
|
||||
return lh.MatchesData([]byte(s))
|
||||
// EqualRaw returns true if the given raw hash digest is equal.
|
||||
// Equality is checked by comparing both the digest value only.
|
||||
// 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.
|
||||
// Deprecated: Use Matches instead.
|
||||
func (lh *LabeledHash) MatchesData(data []byte) bool {
|
||||
hasher := lh.alg.new()
|
||||
_, _ = hasher.Write(data) // never returns an error
|
||||
defer hasher.Reset() // internal state may leak data if kept in memory
|
||||
|
||||
return subtle.ConstantTimeCompare(lh.digest, hasher.Sum(nil)) == 1
|
||||
return lh.Equal(Digest(lh.alg, data))
|
||||
}
|
||||
|
||||
// MatchesString returns true if the digest of the given string matches the hash.
|
||||
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
|
||||
}
|
||||
|
|
|
@ -42,13 +42,13 @@ func testAlgorithm(t *testing.T, alg Algorithm, emptyHex, foxHex string) {
|
|||
}
|
||||
|
||||
// test matching with serialized/loaded labeled hash
|
||||
if !lh.MatchesData(testFoxData) {
|
||||
if !lh.Matches(testFoxData) {
|
||||
t.Errorf("alg %d: failed to match reference", alg)
|
||||
}
|
||||
if !lh.MatchesString(testFox) {
|
||||
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)
|
||||
}
|
||||
if lh.MatchesString(noMatch) {
|
||||
|
@ -99,13 +99,13 @@ func testFormat(t *testing.T, alg Algorithm, lhs, loaded *LabeledHash) {
|
|||
}
|
||||
|
||||
// Test matching.
|
||||
if !loaded.MatchesData(testFoxData) {
|
||||
if !loaded.Matches(testFoxData) {
|
||||
t.Errorf("alg %d: failed to match reference", alg)
|
||||
}
|
||||
if !loaded.MatchesString(testFox) {
|
||||
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)
|
||||
}
|
||||
if loaded.MatchesString(noMatch) {
|
||||
|
|
6
pack
6
pack
|
@ -70,12 +70,18 @@ function check_all {
|
|||
GOOS=linux GOARCH=amd64 check
|
||||
GOOS=windows 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 {
|
||||
GOOS=linux GOARCH=amd64 build
|
||||
GOOS=windows 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 {
|
||||
|
|
|
@ -17,7 +17,10 @@ const (
|
|||
)
|
||||
|
||||
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)
|
||||
)
|
||||
|
|
|
@ -164,6 +164,7 @@ func newSession(e *Envelope) (*Session, error) { //nolint:maintidx
|
|||
|
||||
case tools.PurposeSigning:
|
||||
s.signers = append(s.signers, logic)
|
||||
s.toolRequirements.Add(Integrity)
|
||||
s.toolRequirements.Add(SenderAuthentication)
|
||||
|
||||
case tools.PurposeIntegratedCipher:
|
||||
|
|
67
signet.go
67
signet.go
|
@ -7,9 +7,11 @@ import (
|
|||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/mr-tron/base58"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
|
||||
"github.com/safing/jess/tools"
|
||||
"github.com/safing/portbase/formats/dsd"
|
||||
)
|
||||
|
||||
// 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.
|
||||
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
|
||||
err := signet.LoadKey()
|
||||
if err != nil {
|
||||
|
@ -249,3 +259,60 @@ func (signet *Signet) AssignUUID() error {
|
|||
signet.ID = u.String()
|
||||
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)
|
||||
}
|
||||
|
|
13
suites.go
13
suites.go
|
@ -35,7 +35,16 @@ var (
|
|||
SuiteSignV1 = registerSuite(&Suite{
|
||||
ID: "sign_v1",
|
||||
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,
|
||||
Status: SuiteStatusRecommended,
|
||||
})
|
||||
|
@ -66,6 +75,8 @@ var (
|
|||
SuiteRcptOnly = SuiteRcptOnlyV1
|
||||
// SuiteSign is a a cipher suite for signing (no encryption).
|
||||
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 = SuiteCompleteV1
|
||||
// SuiteWire is a a cipher suite for network communication, including authentication of the server, but not the client.
|
||||
|
|
|
@ -193,6 +193,7 @@ func suiteBullshitCheck(suite *Suite) error { //nolint:maintidx
|
|||
|
||||
case tools.PurposeSigning:
|
||||
s.signers = append(s.signers, logic)
|
||||
s.toolRequirements.Add(Integrity)
|
||||
s.toolRequirements.Add(SenderAuthentication)
|
||||
|
||||
case tools.PurposeIntegratedCipher:
|
||||
|
@ -417,6 +418,7 @@ func computeSuiteAttributes(toolIDs []string, assumeKey bool) *Suite {
|
|||
newSuite.Provides.Add(RecipientAuthentication)
|
||||
|
||||
case tools.PurposeSigning:
|
||||
newSuite.Provides.Add(Integrity)
|
||||
newSuite.Provides.Add(SenderAuthentication)
|
||||
|
||||
case tools.PurposeIntegratedCipher:
|
||||
|
|
1
tools.go
1
tools.go
|
@ -2,7 +2,6 @@ package jess
|
|||
|
||||
import (
|
||||
"github.com/safing/jess/tools"
|
||||
|
||||
// Import all tools.
|
||||
_ "github.com/safing/jess/tools/all"
|
||||
)
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
// Package all imports all tool subpackages
|
||||
//
|
||||
//nolint:gci
|
||||
package all
|
||||
|
||||
import (
|
||||
|
|
|
@ -3,7 +3,7 @@ package gostdlib
|
|||
import (
|
||||
"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"
|
||||
)
|
||||
|
|
|
@ -1,9 +1,15 @@
|
|||
package truststores
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"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.
|
||||
type ExtendedTrustStore interface {
|
||||
jess.TrustStore
|
||||
|
|
|
@ -2,7 +2,6 @@ package truststores
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/safing/jess"
|
||||
|
@ -27,7 +26,7 @@ func WriteSignetToFile(signet *jess.Signet, filename string) error {
|
|||
}
|
||||
|
||||
// write
|
||||
err = ioutil.WriteFile(filename, data, 0o0600)
|
||||
err = os.WriteFile(filename, data, 0o0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -37,7 +36,7 @@ func WriteSignetToFile(signet *jess.Signet, filename string) error {
|
|||
|
||||
// LoadSignetFromFile loads a signet from the given filepath.
|
||||
func LoadSignetFromFile(filename string) (*jess.Signet, error) {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, jess.ErrSignetNotFound
|
||||
|
@ -72,7 +71,7 @@ func WriteEnvelopeToFile(envelope *jess.Envelope, filename string) error {
|
|||
}
|
||||
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
|
@ -82,7 +81,7 @@ func WriteEnvelopeToFile(envelope *jess.Envelope, filename string) error {
|
|||
|
||||
// LoadEnvelopeFromFile loads an envelope from the given filepath.
|
||||
func LoadEnvelopeFromFile(filename string) (*jess.Envelope, error) {
|
||||
data, err := ioutil.ReadFile(filename)
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, jess.ErrEnvelopeNotFound
|
||||
|
|
148
truststores/keyring.go
Normal file
148
truststores/keyring.go
Normal 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
|
||||
}
|
Loading…
Add table
Reference in a new issue