diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index e5d1ca0..d1fa2da 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -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 diff --git a/.gitignore b/.gitignore index 835fb56..831dc4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,7 @@ cpu.out vendor -cmd/cmd* cmd/jess* dist -# Custom dev deops +# Custom dev deps go.mod.* diff --git a/.golangci.yml b/.golangci.yml index a1720fe..8202fea 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -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 diff --git a/cmd/build b/cmd/build index bd347c2..36b9ff6 100755 --- a/cmd/build +++ b/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}" "$@" diff --git a/cmd/cfg-envelope.go b/cmd/cfg-envelope.go index 6d6eb11..48d2020 100644 --- a/cmd/cfg-envelope.go +++ b/cmd/cfg-envelope.go @@ -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"): diff --git a/cmd/cfg-signet.go b/cmd/cfg-signet.go index 9ef217e..9029f7c 100644 --- a/cmd/cfg-signet.go +++ b/cmd/cfg-signet.go @@ -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 + } } } diff --git a/cmd/cmd-close.go b/cmd/cmd-close.go index bec65bb..e35a20a 100644 --- a/cmd/cmd-close.go +++ b/cmd/cmd-close.go @@ -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 diff --git a/cmd/cmd-generate.go b/cmd/cmd-generate.go index e4f9b21..09bff8f 100644 --- a/cmd/cmd-generate.go +++ b/cmd/cmd-generate.go @@ -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 }, } ) diff --git a/cmd/cmd-import-export.go b/cmd/cmd-import-export.go new file mode 100644 index 0000000..f161899 --- /dev/null +++ b/cmd/cmd-import-export.go @@ -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 +} diff --git a/cmd/cmd-open.go b/cmd/cmd-open.go index b8a34e9..4fdadc0 100644 --- a/cmd/cmd-open.go +++ b/cmd/cmd-open.go @@ -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 { diff --git a/cmd/cmd-sign.go b/cmd/cmd-sign.go new file mode 100644 index 0000000..0db49c1 --- /dev/null +++ b/cmd/cmd-sign.go @@ -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 + }, + } +) diff --git a/cmd/cmd-verify.go b/cmd/cmd-verify.go index 6e36ad5..bd0401f 100644 --- a/cmd/cmd-verify.go +++ b/cmd/cmd-verify.go @@ -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 +} diff --git a/cmd/format_sig.go b/cmd/format_sig.go new file mode 100644 index 0000000..1970b2e --- /dev/null +++ b/cmd/format_sig.go @@ -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] } diff --git a/cmd/main.go b/cmd/main.go index 2027948..ef46179 100644 --- a/cmd/main.go +++ b/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) diff --git a/cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.recipient b/cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.recipient new file mode 100644 index 0000000..1958281 --- /dev/null +++ b/cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.recipient @@ -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 + } +} \ No newline at end of file diff --git a/cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.signet b/cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.signet new file mode 100644 index 0000000..ef9764d --- /dev/null +++ b/cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.signet @@ -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 + } +} \ No newline at end of file diff --git a/cmd/testdata/.truststore/safing-codesign-1.envelope b/cmd/testdata/.truststore/safing-codesign-1.envelope new file mode 100644 index 0000000..d853b67 --- /dev/null +++ b/cmd/testdata/.truststore/safing-codesign-1.envelope @@ -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 +} \ No newline at end of file diff --git a/cmd/testdata/test.txt b/cmd/testdata/test.txt new file mode 100644 index 0000000..a042389 --- /dev/null +++ b/cmd/testdata/test.txt @@ -0,0 +1 @@ +hello world! diff --git a/cmd/testdata/test.txt.letter b/cmd/testdata/test.txt.letter new file mode 100644 index 0000000..0646238 --- /dev/null +++ b/cmd/testdata/test.txt.letter @@ -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! diff --git a/cmd/testdata/test.txt.sig b/cmd/testdata/test.txt.sig new file mode 100644 index 0000000..4eaab30 --- /dev/null +++ b/cmd/testdata/test.txt.sig @@ -0,0 +1,9 @@ +-----BEGIN JESS SIGNATURE----- +Q6VnVmVyc2lvbgFnU3VpdGVJRGtzaWduZmlsZV92MWVOb25jZUQb+MqAZERhdGFY +d02Dq0xhYmVsZWRIYXNoxCIJIOz3Afcn2eLXfEqkmsb7vMmXJ4rKAQvd7rlhwQz1 +TUNaqFNpZ25lZEF01v9iy+uLqE1ldGFEYXRhgqd2ZXJzaW9upTAuMC4xqmlkZW50 +aWZpZXKyd2luZG93cy9jb2RlL3RoaW5nalNpZ25hdHVyZXOBo2ZTY2hlbWVnRWQy +NTUxOWJJRHgkMzkxMWM4NGMtNzhmNy00MzU0LWE3ZjUtMGUxMTVhYTI5MDNjZVZh +bHVlWECJZFbIifczUGAJkmATXCHy/MiQZkiktM99X7U/cPgw3IKpKAxQsJ5LobgZ +4P2ecv0IlN4gQb+x+lycxl93E9sJ +-----END JESS SIGNATURE----- \ No newline at end of file diff --git a/cmd/testdata/test3.txt b/cmd/testdata/test3.txt new file mode 100644 index 0000000..25c2c9e --- /dev/null +++ b/cmd/testdata/test3.txt @@ -0,0 +1 @@ +hello world!! diff --git a/cmd/testdata/test3.txt.sig b/cmd/testdata/test3.txt.sig new file mode 100644 index 0000000..f654070 --- /dev/null +++ b/cmd/testdata/test3.txt.sig @@ -0,0 +1,9 @@ +-----BEGIN JESS SIGNATURE----- +Q6VnVmVyc2lvbgFnU3VpdGVJRGtzaWduZmlsZV92MWVOb25jZUQJ9s/nZERhdGFY +d02Dq0xhYmVsZWRIYXNoxCIJILtKnL1AHj7YubrWdLu1D+voud8Ky04vh756eTae +rWQwqFNpZ25lZEF01v9izC6hqE1ldGFEYXRhgqd2ZXJzaW9upTAuMC4xqmlkZW50 +aWZpZXKyd2luZG93cy9jb2RlL3RoaW5nalNpZ25hdHVyZXOBo2ZTY2hlbWVnRWQy +NTUxOWJJRHgkMzkxMWM4NGMtNzhmNy00MzU0LWE3ZjUtMGUxMTVhYTI5MDNjZVZh +bHVlWEBLsd2QbM7VmEsnW60hHn/V6EP2mGFauWZgbEOlKTiqumVFbWU4K7Fi91KL +Zgvwj+CNdZJ7Xv2qR7etviRDCmwC +-----END JESS SIGNATURE----- \ No newline at end of file diff --git a/cmd/testdata/test4.txt b/cmd/testdata/test4.txt new file mode 100644 index 0000000..a042389 --- /dev/null +++ b/cmd/testdata/test4.txt @@ -0,0 +1 @@ +hello world! diff --git a/cmd/testdata/testdir/test2.txt b/cmd/testdata/testdir/test2.txt new file mode 100644 index 0000000..a042389 --- /dev/null +++ b/cmd/testdata/testdir/test2.txt @@ -0,0 +1 @@ +hello world! diff --git a/cmd/testdata/testdir/test2.txt.sig b/cmd/testdata/testdir/test2.txt.sig new file mode 100644 index 0000000..524d2b7 --- /dev/null +++ b/cmd/testdata/testdir/test2.txt.sig @@ -0,0 +1,9 @@ +-----BEGIN JESS SIGNATURE----- +Q6VnVmVyc2lvbgFnU3VpdGVJRGtzaWduZmlsZV92MWVOb25jZUThzxO6ZERhdGFY +d02Dq0xhYmVsZWRIYXNoxCIJIOz3Afcn2eLXfEqkmsb7vMmXJ4rKAQvd7rlhwQz1 +TUNaqFNpZ25lZEF01v9izC3SqE1ldGFEYXRhgqd2ZXJzaW9upTAuMC4xqmlkZW50 +aWZpZXKyd2luZG93cy9jb2RlL3RoaW5nalNpZ25hdHVyZXOBo2ZTY2hlbWVnRWQy +NTUxOWJJRHgkMzkxMWM4NGMtNzhmNy00MzU0LWE3ZjUtMGUxMTVhYTI5MDNjZVZh +bHVlWEAGLkIoej0+ilJrIyb+BzX8+Yw2LY0zkoL9vwI02/2KqKVT7/pH+LTDX1Hl +h1epYkF8ICdwa1iVNDx6P7iNmWkL +-----END JESS SIGNATURE----- \ No newline at end of file diff --git a/core_test.go b/core_test.go index e79046c..ea07472 100644 --- a/core_test.go +++ b/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)) diff --git a/envelope.go b/envelope.go index 48d0430..71c2aa8 100644 --- a/envelope.go +++ b/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) +} diff --git a/filesig/format_armor.go b/filesig/format_armor.go new file mode 100644 index 0000000..78e38e9 --- /dev/null +++ b/filesig/format_armor.go @@ -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 +} diff --git a/filesig/format_armor_test.go b/filesig/format_armor_test.go new file mode 100644 index 0000000..85c8e55 --- /dev/null +++ b/filesig/format_armor_test.go @@ -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)) + } +} diff --git a/filesig/helpers.go b/filesig/helpers.go new file mode 100644 index 0000000..90e7918 --- /dev/null +++ b/filesig/helpers.go @@ -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 +} diff --git a/filesig/main.go b/filesig/main.go new file mode 100644 index 0000000..b71ad87 --- /dev/null +++ b/filesig/main.go @@ -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 +} diff --git a/filesig/main_test.go b/filesig/main_test.go new file mode 100644 index 0000000..7f8a0ff --- /dev/null +++ b/filesig/main_test.go @@ -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 +} diff --git a/go.mod b/go.mod index a7b622d..bebc3d5 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 7625d9f..c41bff6 100644 --- a/go.sum +++ b/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= diff --git a/hashtools/blake2.go b/hashtools/blake2.go index ad4b876..a2262cd 100644 --- a/hashtools/blake2.go +++ b/hashtools/blake2.go @@ -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, })) } diff --git a/hashtools/hashtool.go b/hashtools/hashtool.go index 664442c..a86fff2 100644 --- a/hashtools/hashtool.go +++ b/hashtools/hashtool.go @@ -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 +} diff --git a/hashtools/sha.go b/hashtools/sha.go index eb1e947..ea16311 100644 --- a/hashtools/sha.go +++ b/hashtools/sha.go @@ -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, })) } diff --git a/import_export.go b/import_export.go new file mode 100644 index 0000000..84eccdd --- /dev/null +++ b/import_export.go @@ -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, "-"), "-", + ), + ) +} diff --git a/lhash/algs.go b/lhash/algs.go index 948d7fe..e4a15cc 100644 --- a/lhash/algs.go +++ b/lhash/algs.go @@ -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) +} diff --git a/lhash/labeledhash.go b/lhash/labeledhash.go index 55c42a6..68e5475 100644 --- a/lhash/labeledhash.go +++ b/lhash/labeledhash.go @@ -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 } diff --git a/lhash/labeledhash_test.go b/lhash/labeledhash_test.go index 8ed91f1..5facd3c 100644 --- a/lhash/labeledhash_test.go +++ b/lhash/labeledhash_test.go @@ -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) { diff --git a/pack b/pack index 6317112..3c94947 100755 --- a/pack +++ b/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 { diff --git a/session-wire.go b/session-wire.go index ad80cb6..95d7c5c 100644 --- a/session-wire.go +++ b/session-wire.go @@ -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) ) diff --git a/session.go b/session.go index fd29cac..24d5d2a 100644 --- a/session.go +++ b/session.go @@ -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: diff --git a/signet.go b/signet.go index 4913f19..9a950d8 100644 --- a/signet.go +++ b/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) +} diff --git a/suites.go b/suites.go index ce01b38..bfaaf30 100644 --- a/suites.go +++ b/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. diff --git a/suites_test.go b/suites_test.go index ec06de4..277f63a 100644 --- a/suites_test.go +++ b/suites_test.go @@ -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: diff --git a/tools.go b/tools.go index 605e0bb..2564ea3 100644 --- a/tools.go +++ b/tools.go @@ -2,7 +2,6 @@ package jess import ( "github.com/safing/jess/tools" - // Import all tools. _ "github.com/safing/jess/tools/all" ) diff --git a/tools/all/all.go b/tools/all/all.go index 8d59fb8..b1d0080 100644 --- a/tools/all/all.go +++ b/tools/all/all.go @@ -1,6 +1,4 @@ // Package all imports all tool subpackages -// -//nolint:gci package all import ( diff --git a/tools/gostdlib/poly1305.go b/tools/gostdlib/poly1305.go index e1640f3..ef4153b 100644 --- a/tools/gostdlib/poly1305.go +++ b/tools/gostdlib/poly1305.go @@ -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" ) diff --git a/truststores/extended.go b/truststores/extended.go index 06c41b3..5d41e6c 100644 --- a/truststores/extended.go +++ b/truststores/extended.go @@ -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 diff --git a/truststores/io.go b/truststores/io.go index 1c795d5..8ccff5e 100644 --- a/truststores/io.go +++ b/truststores/io.go @@ -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 diff --git a/truststores/keyring.go b/truststores/keyring.go new file mode 100644 index 0000000..1132b53 --- /dev/null +++ b/truststores/keyring.go @@ -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 +}