Merge pull request from safing/feature/file-sigs

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

View file

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

3
.gitignore vendored
View file

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

View file

@ -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

View file

@ -50,6 +50,12 @@ echo "Please notice, that this build script includes metadata into the build."
echo "This information is useful for debugging and license compliance."
echo "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}" "$@"

View file

@ -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"):

View file

@ -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
}
}
}

View file

@ -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

View file

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

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

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

View file

@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"strings"
@ -79,9 +78,9 @@ var (
// load file
var data []byte
if filename == "-" {
data, err = ioutil.ReadAll(os.Stdin)
data, err = io.ReadAll(os.Stdin)
} else {
data, err = ioutil.ReadFile(filename)
data, err = os.ReadFile(filename)
}
if err != nil {
return err
@ -93,6 +92,11 @@ var (
return err
}
// Create default requirements if not set.
if requirements == nil {
requirements = jess.NewRequirements()
}
// decrypt (and verify)
plainText, err := letter.Open(requirements, trustStore)
if err != nil {

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

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

View file

@ -3,85 +3,260 @@ package main
import (
"errors"
"fmt"
"io/ioutil"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"github.com/safing/jess"
"github.com/safing/jess/filesig"
"github.com/safing/portbase/container"
)
func init() {
rootCmd.AddCommand(verifyCmd)
verifyCmd.Flags().StringToStringVarP(&metaDataFlag, "metadata", "m", nil, "specify file metadata to verify (.sig only)")
}
var (
verifyCmdHelp = "usage: jess verify <file>"
var verifyCmd = &cobra.Command{
Use: "verify <files and directories>",
Short: "verify signed files and files in directories",
DisableFlagsInUseLine: true,
Args: cobra.MinimumNArgs(1),
PreRunE: requireTrustStore,
RunE: func(cmd *cobra.Command, args []string) (err error) {
var verificationFails, verificationWarnings int
verifyCmd = &cobra.Command{
Use: "verify <file>",
Short: "verify file",
DisableFlagsInUseLine: true,
RunE: func(cmd *cobra.Command, args []string) (err error) {
// check args
if len(args) != 1 {
return errors.New(verifyCmdHelp)
// Check if we are only verifying a single file.
if len(args) == 1 {
matches, err := filepath.Glob(args[0])
if err != nil {
return err
}
// check filenames
filename := args[0]
// check input file
if filename != "-" {
fileInfo, err := os.Stat(filename)
switch len(matches) {
case 0:
return errors.New("file not found")
case 1:
// Check if the single match is a file.
fileInfo, err := os.Stat(matches[0])
if err != nil {
return err
}
if fileInfo.Size() > warnFileSize {
confirmed, err := confirm("Input file is really big (%s) and jess needs to load it fully to memory, continue?", true)
if err != nil {
return err
}
if !confirmed {
return nil
}
// Verify file if it is not a directory.
if !fileInfo.IsDir() {
return verify(matches[0], false)
}
}
}
// load file
var data []byte
if filename == "-" {
data, err = ioutil.ReadAll(os.Stdin)
} else {
data, err = ioutil.ReadFile(filename)
}
// Resolve globs.
files := make([]string, 0, len(args))
for _, arg := range args {
matches, err := filepath.Glob(arg)
if err != nil {
return err
}
files = append(files, matches...)
}
// parse file
letter, err := jess.LetterFromFileFormat(container.New(data))
// Go through all files.
for _, file := range files {
fileInfo, err := os.Stat(file)
if err != nil {
return err
verificationWarnings++
fmt.Printf("[WARN] %s failed to read: %s\n", file, err)
continue
}
// adjust requirements
if requirements == nil {
requirements = jess.NewRequirements().
Remove(jess.Confidentiality).
Remove(jess.Integrity).
Remove(jess.RecipientAuthentication)
// Walk directories.
if fileInfo.IsDir() {
err := filepath.Walk(file, func(path string, info fs.FileInfo, err error) error {
// Log walking errors.
if err != nil {
verificationWarnings++
fmt.Printf("[WARN] %s failed to read: %s\n", path, err)
return nil
}
// Only verify if .sig or .letter.
if strings.HasSuffix(path, filesig.Extension) ||
strings.HasSuffix(path, letterFileExtension) {
if err := verify(path, true); err != nil {
verificationFails++
}
}
return nil
})
if err != nil {
verificationWarnings++
fmt.Printf("[WARN] %s failed to walk directory: %s\n", file, err)
}
continue
}
// verify
err = letter.Verify(requirements, trustStore)
if err != nil {
return err
if err := verify(file, true); err != nil {
verificationFails++
}
}
// success
fmt.Println("ok")
return nil
},
// End with error status if any verification failed.
if verificationFails > 0 {
return fmt.Errorf("%d verification failures", verificationFails)
}
if verificationWarnings > 0 {
return fmt.Errorf("%d warnings", verificationWarnings)
}
return nil
},
}
var verifiedSigs = make(map[string]struct{})
func verify(filename string, bulkMode bool) error {
// Check if file was already verified.
if _, alreadyVerified := verifiedSigs[filename]; alreadyVerified {
return nil
}
)
var (
signame string
signedBy []string
err error
)
// Get correct files and verify.
switch {
case filename == stdInOutFilename:
signedBy, err = verifyLetter(filename, bulkMode)
case strings.HasSuffix(filename, letterFileExtension):
signedBy, err = verifyLetter(filename, bulkMode)
case strings.HasSuffix(filename, filesig.Extension):
filename = strings.TrimSuffix(filename, filesig.Extension)
fallthrough
default:
signame = filename + filesig.Extension
signedBy, err = verifySig(filename, signame, bulkMode)
}
// Remember the files already verified.
verifiedSigs[filename] = struct{}{}
if signame != "" {
verifiedSigs[signame] = struct{}{}
}
// Output result in bulk mode.
if bulkMode {
if err == nil {
fmt.Printf("[ OK ] %s signed by %s\n", filename, strings.Join(signedBy, ", "))
} else {
fmt.Printf("[FAIL] %s failed to verify: %s\n", filename, err)
}
}
return err
}
func verifyLetter(filename string, silent bool) (signedBy []string, err error) {
if len(metaDataFlag) > 0 {
return nil, errors.New("metadata flag only valid for verifying .sig files")
}
if filename != "-" {
fileInfo, err := os.Stat(filename)
if err != nil {
return nil, err
}
if fileInfo.Size() > warnFileSize {
confirmed, err := confirm("Input file is really big (%s) and jess needs to load it fully to memory, continue?", true)
if err != nil {
return nil, err
}
if !confirmed {
return nil, nil
}
}
}
// load file
var data []byte
if filename == "-" {
data, err = io.ReadAll(os.Stdin)
} else {
data, err = os.ReadFile(filename)
}
if err != nil {
return nil, err
}
// parse file
letter, err := jess.LetterFromFileFormat(container.New(data))
if err != nil {
return nil, err
}
// Create default requirements if not set.
if requirements == nil {
requirements = jess.NewRequirements().
Remove(jess.Confidentiality).
Remove(jess.RecipientAuthentication)
}
// verify
err = letter.Verify(requirements, trustStore)
if err != nil {
return nil, err
}
// get signers
signedBy = make([]string, 0, len(letter.Signatures))
for _, seal := range letter.Signatures {
if signet, err := trustStore.GetSignet(seal.ID, true); err == nil {
signedBy = append(signedBy, fmt.Sprintf("%s (%s)", signet.Info.Name, seal.ID))
} else {
signedBy = append(signedBy, seal.ID)
}
}
// success
if !silent {
if err == nil {
fmt.Println("Verification: OK")
fmt.Printf("Signed By: %s\n", strings.Join(signedBy, ", "))
} else {
fmt.Printf("Verification FAILED: %s\n\n", err)
}
}
return signedBy, nil
}
func verifySig(filename, signame string, silent bool) (signedBy []string, err error) {
fds, err := filesig.VerifyFile(filename, signame, metaDataFlag, trustStore)
if err != nil {
return nil, err
}
if !silent {
fmt.Print(formatSignatures(filename, signame, fds))
return nil, nil
}
signedBy = make([]string, 0, len(fds))
for _, fd := range fds {
if sig := fd.Signature(); sig != nil {
for _, seal := range sig.Signatures {
if signet, err := trustStore.GetSignet(seal.ID, true); err == nil {
signedBy = append(signedBy, fmt.Sprintf("%s (%s)", signet.Info.Name, seal.ID))
} else {
signedBy = append(signedBy, seal.ID)
}
}
}
}
return signedBy, nil
}

79
cmd/format_sig.go Normal file
View file

@ -0,0 +1,79 @@
package main
import (
"encoding/hex"
"fmt"
"sort"
"strings"
"github.com/safing/jess/filesig"
)
func formatSignatures(filename, signame string, fds []*filesig.FileData) string {
b := &strings.Builder{}
switch len(fds) {
case 0:
case 1:
formatSignature(b, fds[0])
case 2:
for _, fd := range fds {
fmt.Fprintf(b, "%d Signatures:\n\n\n", len(fds))
formatSignature(b, fd)
b.WriteString("\n\n")
}
}
if filename != "" || signame != "" {
b.WriteString("\n")
fmt.Fprintf(b, "File: %s\n", filename)
fmt.Fprintf(b, "Sig: %s\n", signame)
}
return b.String()
}
func formatSignature(b *strings.Builder, fd *filesig.FileData) {
if fd.VerificationError() == nil {
b.WriteString("Verification: OK\n")
} else {
fmt.Fprintf(b, "Verification FAILED: %s\n", fd.VerificationError())
}
if letter := fd.Signature(); letter != nil {
b.WriteString("\n")
for _, sig := range letter.Signatures {
signet, err := trustStore.GetSignet(sig.ID, true)
if err == nil {
fmt.Fprintf(b, "Signed By: %s (%s)\n", signet.Info.Name, sig.ID)
} else {
fmt.Fprintf(b, "Signed By: %s\n", sig.ID)
}
}
}
if fileHash := fd.FileHash(); fileHash != nil {
b.WriteString("\n")
fmt.Fprintf(b, "Hash Alg: %s\n", fileHash.Algorithm())
fmt.Fprintf(b, "Hash Sum: %s\n", hex.EncodeToString(fileHash.Sum()))
}
if len(fd.MetaData) > 0 {
b.WriteString("\nMetadata:\n")
sortedMetaData := make([][]string, 0, len(fd.MetaData))
for k, v := range fd.MetaData {
sortedMetaData = append(sortedMetaData, []string{k, v})
}
sort.Sort(sortByMetaDataKey(sortedMetaData))
for _, entry := range sortedMetaData {
fmt.Fprintf(b, " %s: %s\n", entry[0], entry[1])
}
}
}
type sortByMetaDataKey [][]string
func (a sortByMetaDataKey) Len() int { return len(a) }
func (a sortByMetaDataKey) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
func (a sortByMetaDataKey) Less(i, j int) bool { return a[i][0] < a[j][0] }

View file

@ -14,6 +14,9 @@ import (
)
const (
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)

View file

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

View file

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

View file

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

View file

@ -225,10 +225,8 @@ func TestCoreAllCombinations(t *testing.T) {
t.Logf("of these, %d were successfully detected as invalid", combinationsDetectedInvalid)
}
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))

View file

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

123
filesig/format_armor.go Normal file
View file

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

View file

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

146
filesig/helpers.go Normal file
View file

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

123
filesig/main.go Normal file
View file

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

130
filesig/main_test.go Normal file
View file

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

1
go.mod
View file

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

8
go.sum
View file

@ -71,6 +71,8 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/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=

View file

@ -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,
}))
}

View file

@ -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
}

View file

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

195
import_export.go Normal file
View file

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

View file

@ -6,6 +6,7 @@ package lhash
import (
"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)
}

View file

@ -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
}

View file

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

6
pack
View file

@ -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 {

View file

@ -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)
)

View file

@ -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:

View file

@ -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)
}

View file

@ -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.

View file

@ -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:

View file

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

View file

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

View file

@ -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"
)

View file

@ -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

View file

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

148
truststores/keyring.go Normal file
View file

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