From 9dffde4dfafd85460d7c4ac51b3b053a2a0467f3 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Fri, 8 Jul 2022 22:30:55 +0200 Subject: [PATCH 01/31] Add Integrity attribute to Signing Purpose --- core_test.go | 1 + session.go | 1 + suites.go | 2 +- suites_test.go | 2 ++ 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/core_test.go b/core_test.go index e79046c..045abd3 100644 --- a/core_test.go +++ b/core_test.go @@ -378,6 +378,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) 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/suites.go b/suites.go index ce01b38..2fc682d 100644 --- a/suites.go +++ b/suites.go @@ -35,7 +35,7 @@ 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, }) 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: From 6889bbd62e8c015d9966fb347fd3d45187f77092 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Fri, 8 Jul 2022 22:31:52 +0200 Subject: [PATCH 02/31] Add reference from hashtools to their labeled hash version --- hashtools/blake2.go | 6 ++++++ hashtools/hashtool.go | 12 ++++++++++++ hashtools/sha.go | 13 ++++++++++++- 3 files changed, 30 insertions(+), 1 deletion(-) 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, })) } From 62077eefb579cd0acd5ee0af2663a7c3477b506e Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Fri, 8 Jul 2022 22:32:13 +0200 Subject: [PATCH 03/31] Add labeled hash helper functions for hashing data --- lhash/algs.go | 16 ++++++++++++++++ lhash/labeledhash.go | 45 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/lhash/algs.go b/lhash/algs.go index 948d7fe..a6f1377 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,18 @@ func (a Algorithm) new() hash.Hash { return nil } } + +// 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(alg Algorithm, reader io.Reader) (*LabeledHash, error) { + return DigestFromReader(a, reader) +} diff --git a/lhash/labeledhash.go b/lhash/labeledhash.go index 55c42a6..58951cd 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.OpenFile(pathToFile, os.O_RDONLY, 0) + 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() From 55378bda8511490a162b72a5a6dca283daca0060 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Fri, 8 Jul 2022 22:32:43 +0200 Subject: [PATCH 04/31] Add signet import/export functions --- signet.go | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/signet.go b/signet.go index 4913f19..367abf4 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. @@ -249,3 +251,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) +} From 8ad0eabf82eb1885bd3d7136f14ba64905d9319a Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Fri, 8 Jul 2022 22:32:53 +0200 Subject: [PATCH 05/31] Fix linter errors --- tools.go | 1 - tools/all/all.go | 2 -- tools/gostdlib/poly1305.go | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) 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" ) From 0bb5f33c4a337189bef1fefe778a068096acf0fa Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Fri, 8 Jul 2022 22:33:30 +0200 Subject: [PATCH 06/31] Add filesig package for signing files in a common way --- filesig/format_armor.go | 123 ++++++++++++++++++++++ filesig/format_armor_test.go | 197 +++++++++++++++++++++++++++++++++++ filesig/helpers.go | 119 +++++++++++++++++++++ filesig/main.go | 112 ++++++++++++++++++++ filesig/main_test.go | 130 +++++++++++++++++++++++ 5 files changed, 681 insertions(+) create mode 100644 filesig/format_armor.go create mode 100644 filesig/format_armor_test.go create mode 100644 filesig/helpers.go create mode 100644 filesig/main.go create mode 100644 filesig/main_test.go diff --git a/filesig/format_armor.go b/filesig/format_armor.go new file mode 100644 index 0000000..3806582 --- /dev/null +++ b/filesig/format_armor.go @@ -0,0 +1,123 @@ +package jess + +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..82928f6 --- /dev/null +++ b/filesig/format_armor_test.go @@ -0,0 +1,197 @@ +package jess + +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..55d0989 --- /dev/null +++ b/filesig/helpers.go @@ -0,0 +1,119 @@ +package jess + +import ( + "errors" + "fmt" + "io/ioutil" + "strings" + + "github.com/safing/jess" + "github.com/safing/jess/hashtools" +) + +// SignFile signs a file and replaces the signature file with a new one. +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, "(")[0], "()") + hashTool, _ = hashtools.Get(hashToolID) + break + } + } + if hashTool == nil { + return nil, errors.New("suite not suitable for file signing") + } + + // Hash the data file. + 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) + } + + // Make signature section for saving to disk. + signatureSection, err := MakeSigFileSection(signature) + if err != nil { + return nil, fmt.Errorf("failed to format signature for file: %w", err) + } + + // Write the signature to file. + if err := ioutil.WriteFile(signatureFilePath, signatureSection, 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 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 := ioutil.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. + 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..b8376e4 --- /dev/null +++ b/filesig/main.go @@ -0,0 +1,112 @@ +package jess + +import ( + "fmt" + "time" + + "github.com/safing/jess" + "github.com/safing/jess/lhash" + "github.com/safing/portbase/formats/dsd" +) + +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 + + verificationError error +} + +// FileHash returns the labeled hash of the file that was signed. +func (fd *FileData) FileHash() *lhash.LabeledHash { + return fd.fileHash +} + +// 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{} + _, 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..c6920c4 --- /dev/null +++ b/filesig/main_test.go @@ -0,0 +1,130 @@ +package jess + +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 +} From 9f017c0f9ee878139d9f0d316283ba2fd5c9340e Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 11 Jul 2022 16:59:25 +0200 Subject: [PATCH 07/31] Add suite for file signing --- suites.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/suites.go b/suites.go index 2fc682d..bfaaf30 100644 --- a/suites.go +++ b/suites.go @@ -39,6 +39,15 @@ var ( 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, + }) // SuiteCompleteV1 is a cipher suite for both encrypting for someone and signing. SuiteCompleteV1 = registerSuite(&Suite{ ID: "v1", @@ -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. From 8e4665a639ae0a043e7dadda1512298be0f24d18 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:00:04 +0200 Subject: [PATCH 08/31] Expose labeled hash algorithm names --- lhash/algs.go | 44 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/lhash/algs.go b/lhash/algs.go index a6f1377..635555d 100644 --- a/lhash/algs.go +++ b/lhash/algs.go @@ -85,6 +85,48 @@ func (a Algorithm) new() hash.Hash { } } +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" + } +} + // Digest creates a new labeled hash and digests the given data. func (a Algorithm) Digest(data []byte) *LabeledHash { return Digest(a, data) @@ -96,6 +138,6 @@ func (a Algorithm) DigestFile(pathToFile string) (*LabeledHash, error) { } // DigestFromReader creates a new labeled hash and digests from the given reader. -func (a Algorithm) DigestFromReader(alg Algorithm, reader io.Reader) (*LabeledHash, error) { +func (a Algorithm) DigestFromReader(reader io.Reader) (*LabeledHash, error) { return DigestFromReader(a, reader) } From d398ae6956b4f8ef659739ea8e7c781c89e9ea2a Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:01:17 +0200 Subject: [PATCH 09/31] Fix filesig package name --- filesig/format_armor.go | 2 +- filesig/format_armor_test.go | 2 +- filesig/helpers.go | 2 +- filesig/main.go | 2 +- filesig/main_test.go | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/filesig/format_armor.go b/filesig/format_armor.go index 3806582..78e38e9 100644 --- a/filesig/format_armor.go +++ b/filesig/format_armor.go @@ -1,4 +1,4 @@ -package jess +package filesig import ( "bytes" diff --git a/filesig/format_armor_test.go b/filesig/format_armor_test.go index 82928f6..85c8e55 100644 --- a/filesig/format_armor_test.go +++ b/filesig/format_armor_test.go @@ -1,4 +1,4 @@ -package jess +package filesig import ( "bytes" diff --git a/filesig/helpers.go b/filesig/helpers.go index 55d0989..91e19cb 100644 --- a/filesig/helpers.go +++ b/filesig/helpers.go @@ -1,4 +1,4 @@ -package jess +package filesig import ( "errors" diff --git a/filesig/main.go b/filesig/main.go index b8376e4..708b007 100644 --- a/filesig/main.go +++ b/filesig/main.go @@ -1,4 +1,4 @@ -package jess +package filesig import ( "fmt" diff --git a/filesig/main_test.go b/filesig/main_test.go index c6920c4..7f8a0ff 100644 --- a/filesig/main_test.go +++ b/filesig/main_test.go @@ -1,4 +1,4 @@ -package jess +package filesig import ( "errors" From a33fe9b9cf0fb2966e71c415154c0abd55639c7f Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:02:17 +0200 Subject: [PATCH 10/31] Add support for verifying files from stdin --- filesig/helpers.go | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/filesig/helpers.go b/filesig/helpers.go index 91e19cb..188ff21 100644 --- a/filesig/helpers.go +++ b/filesig/helpers.go @@ -4,13 +4,17 @@ import ( "errors" "fmt" "io/ioutil" + "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 { @@ -21,7 +25,7 @@ func SignFile(dataFilePath, signatureFilePath string, metaData map[string]string var hashTool *hashtools.HashTool for _, toolID := range envelope.Suite().Tools { if strings.Contains(toolID, "(") { - hashToolID := strings.Trim(strings.Split(toolID, "(")[0], "()") + hashToolID := strings.Trim(strings.Split(toolID, "(")[1], "()") hashTool, _ = hashtools.Get(hashToolID) break } @@ -31,7 +35,12 @@ func SignFile(dataFilePath, signatureFilePath string, metaData map[string]string } // Hash the data file. - fileHash, err := hashTool.LabeledHasher().DigestFile(dataFilePath) + 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) } @@ -57,6 +66,7 @@ func SignFile(dataFilePath, signatureFilePath string, metaData map[string]string } // 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) { @@ -95,7 +105,12 @@ func VerifyFile(dataFilePath, signatureFilePath string, metaData map[string]stri } // Hash the file. - fileHash, err := fileData.FileHash().Algorithm().DigestFile(dataFilePath) + 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 From c346404d6214a33df7db90e6463bb4d4e12d13ae Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:02:46 +0200 Subject: [PATCH 11/31] Add signature to existing file by default and remove other jess sigs --- filesig/helpers.go | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/filesig/helpers.go b/filesig/helpers.go index 188ff21..0037a9c 100644 --- a/filesig/helpers.go +++ b/filesig/helpers.go @@ -51,14 +51,27 @@ func SignFile(dataFilePath, signatureFilePath string, metaData map[string]string return nil, fmt.Errorf("failed to sign file: %w", err) } - // Make signature section for saving to disk. - signatureSection, err := MakeSigFileSection(signature) - if err != nil { - return nil, fmt.Errorf("failed to format signature for file: %w", err) + sigFileData, err := ioutil.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 := ioutil.WriteFile(signatureFilePath, signatureSection, 0o0644); err != nil { //nolint:gosec + if err := ioutil.WriteFile(signatureFilePath, newSigFileData, 0o0644); err != nil { //nolint:gosec return nil, fmt.Errorf("failed to write signature to file: %w", err) } From 4556eda901844ff4ef3e9b130e01f241210a3eb2 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:03:12 +0200 Subject: [PATCH 12/31] Save signature to file data for access outside helper functions --- filesig/main.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/filesig/main.go b/filesig/main.go index 708b007..b4f54ba 100644 --- a/filesig/main.go +++ b/filesig/main.go @@ -21,6 +21,7 @@ type FileData struct { SignedAt time.Time MetaData map[string]string + signature *jess.Letter verificationError error } @@ -29,6 +30,11 @@ 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 @@ -75,7 +81,9 @@ func SignFileData(fileHash *lhash.LabeledHash, metaData map[string]string, envel // 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{} + 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) From 028a0b0d205e1cf4ced97c7894799b10eabeaf96 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:03:35 +0200 Subject: [PATCH 13/31] Fix build script --- cmd/build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/build b/cmd/build index bd347c2..ab409b7 100755 --- a/cmd/build +++ b/cmd/build @@ -51,5 +51,5 @@ 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 -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 -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}" "$@" From 345ceb01e464a2358a6b8fea1e1f9d86847fe706 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:04:01 +0200 Subject: [PATCH 14/31] Stop saving keys to disk in envelopes --- cmd/cfg-envelope.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/cmd/cfg-envelope.go b/cmd/cfg-envelope.go index 6d6eb11..8a58686 100644 --- a/cmd/cfg-envelope.go +++ b/cmd/cfg-envelope.go @@ -55,7 +55,7 @@ func newEnvelope(name string) (*jess.Envelope, error) { envelope.SuiteID = jess.SuiteRcptOnly err = selectSignets(envelope, "recipient") case "Sign a file": - envelope.SuiteID = jess.SuiteSign + envelope.SuiteID = jess.SuiteSignFileV1 err = selectSignets(envelope, "sender") } if err != nil { @@ -105,7 +105,20 @@ func editEnvelope(envelope *jess.Envelope) error { switch { case strings.HasPrefix(submenu, "Done"): - // save + // Check if the envolope is valid. + if envelope.SecurityLevel == 0 { + fmt.Println("Envelope is invalid, please fix before saving.") + continue + } + // Remove and keys and save. + _ = envelope.LoopSecrets("", func(signet *jess.Signet) error { + signet.Key = nil + return nil + }) + _ = envelope.LoopSenders("", func(signet *jess.Signet) error { + signet.Key = nil + return nil + }) return trustStore.StoreEnvelope(envelope) case strings.HasPrefix(submenu, "Abort"): return nil From 74d41194cc61044571a600775e9d68c0f8d358a6 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 11 Jul 2022 17:04:48 +0200 Subject: [PATCH 15/31] Add support for file signatures in cli --- cmd/cmd-close.go | 6 +- cmd/cmd-open.go | 5 + cmd/cmd-verify.go | 271 ++++++++++++++++++++++++++++++++++++++-------- cmd/format_sig.go | 79 ++++++++++++++ cmd/main.go | 7 +- 5 files changed, 315 insertions(+), 53 deletions(-) create mode 100644 cmd/format_sig.go diff --git a/cmd/cmd-close.go b/cmd/cmd-close.go index bec65bb..62e7e2f 100644 --- a/cmd/cmd-close.go +++ b/cmd/cmd-close.go @@ -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 != "-" { diff --git a/cmd/cmd-open.go b/cmd/cmd-open.go index b8a34e9..edf3ecf 100644 --- a/cmd/cmd-open.go +++ b/cmd/cmd-open.go @@ -93,6 +93,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-verify.go b/cmd/cmd-verify.go index 6e36ad5..1efc832 100644 --- a/cmd/cmd-verify.go +++ b/cmd/cmd-verify.go @@ -3,85 +3,260 @@ package main import ( "errors" "fmt" + "io/fs" "io/ioutil" "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, sigFileExtension) || + strings.HasSuffix(path, letterFileExtension) { + if err := verify(path, true); err != nil { + verificationFails++ + } + } + return nil + }) + if err != nil { + verificationWarnings++ + fmt.Printf("[WARN] %s failed to walk directory: %s\n", file, err) + } + continue } - // 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, sigFileExtension): + filename = strings.TrimSuffix(filename, sigFileExtension) + fallthrough + default: + signame = filename + sigFileExtension + signedBy, err = verifySig(filename, signame, bulkMode) + } + + // Remember the files already verified. + verifiedSigs[filename] = struct{}{} + if signame != "" { + verifiedSigs[signame] = struct{}{} + } + + // Output result in bulk mode. + if bulkMode { + if err == nil { + fmt.Printf("[ OK ] %s signed by %s\n", filename, strings.Join(signedBy, ", ")) + } else { + fmt.Printf("[FAIL] %s failed to verify: %s\n", filename, err) + } + } + + return err +} + +func verifyLetter(filename string, silent bool) (signedBy []string, err error) { + if len(metaDataFlag) > 0 { + return nil, errors.New("metadata flag only valid for verifying .sig files") + } + + 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 = ioutil.ReadAll(os.Stdin) + } else { + data, err = ioutil.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..93ad9d5 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -14,6 +14,10 @@ import ( ) const ( + stdInOutFilename = "-" + letterFileExtension = ".letter" + sigFileExtension = ".sig" + warnFileSize = 12000000 // 120MB ) @@ -33,7 +37,7 @@ var ( defaultSymmetricKeySize = 0 trustStore truststores.ExtendedTrustStore - requirements = jess.NewRequirements() + requirements *jess.Requirements ) func main() { @@ -74,7 +78,6 @@ func initGlobalFlags(cmd *cobra.Command, args []string) (err error) { return err } } - // requirements if noSpec != "" { requirements, err = jess.ParseRequirementsFromNoSpec(noSpec) From d4d06574b80a874fca0f2eaf2f050fead33b401b Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 8 Aug 2022 14:45:06 +0200 Subject: [PATCH 16/31] Add text format for signets and envelopes and support for import/export --- cmd/cfg-envelope.go | 28 ++- ...c84c-78f7-4354-a7f5-0e115aa2903c.recipient | 14 ++ ...911c84c-78f7-4354-a7f5-0e115aa2903c.signet | 13 ++ .../.truststore/safing-codesign-1.envelope | 23 +++ cmd/testdata/test.txt | 1 + cmd/testdata/test.txt.letter | 13 ++ cmd/testdata/test.txt.sig | 9 + cmd/testdata/test3.txt | 1 + cmd/testdata/test3.txt.sig | 9 + cmd/testdata/test4.txt | 1 + cmd/testdata/testdir/test2.txt | 1 + cmd/testdata/testdir/test2.txt.sig | 9 + envelope.go | 81 ++++++++ import_export.go | 192 ++++++++++++++++++ signet.go | 8 + 15 files changed, 393 insertions(+), 10 deletions(-) create mode 100644 cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.recipient create mode 100644 cmd/testdata/.truststore/3911c84c-78f7-4354-a7f5-0e115aa2903c.signet create mode 100644 cmd/testdata/.truststore/safing-codesign-1.envelope create mode 100644 cmd/testdata/test.txt create mode 100644 cmd/testdata/test.txt.letter create mode 100644 cmd/testdata/test.txt.sig create mode 100644 cmd/testdata/test3.txt create mode 100644 cmd/testdata/test3.txt.sig create mode 100644 cmd/testdata/test4.txt create mode 100644 cmd/testdata/testdir/test2.txt create mode 100644 cmd/testdata/testdir/test2.txt.sig create mode 100644 import_export.go diff --git a/cmd/cfg-envelope.go b/cmd/cfg-envelope.go index 8a58686..873ac6b 100644 --- a/cmd/cfg-envelope.go +++ b/cmd/cfg-envelope.go @@ -30,7 +30,7 @@ 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)", }, } err := survey.AskOne(prompt, &preset, nil) @@ -93,6 +93,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,21 +106,28 @@ func editEnvelope(envelope *jess.Envelope) error { switch { case strings.HasPrefix(submenu, "Done"): - // Check if the envolope is valid. + // 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.LoopSecrets("", func(signet *jess.Signet) error { - signet.Key = nil - return nil - }) - _ = envelope.LoopSenders("", func(signet *jess.Signet) error { - signet.Key = nil - return nil - }) + 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/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/envelope.go b/envelope.go index 48d0430..2935041 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.Secrets[i] = &Signet{ + Version: signet.Version, + ID: signet.ID, + Scheme: signet.Scheme, + } + } + for i, signet := range e.Recipients { + e.Secrets[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/import_export.go b/import_export.go new file mode 100644 index 0000000..347d5c2 --- /dev/null +++ b/import_export.go @@ -0,0 +1,192 @@ +package jess + +import ( + "errors" + "fmt" + "regexp" + "strings" +) + +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-z\-]+`) + +// toTextFormatString makes a string compatible with the text format. +func toTextFormatString(s string) string { + return strings.ToLower( + replaceForTextFormatMatcher.ReplaceAllString(s, "_"), + ) +} diff --git a/signet.go b/signet.go index 367abf4..9a950d8 100644 --- a/signet.go +++ b/signet.go @@ -136,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 { From c0d050e34cd3eea18377303f9641bbfcd6905431 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 8 Aug 2022 14:45:53 +0200 Subject: [PATCH 17/31] Add support for system keyring as trust store --- cmd/main.go | 31 +++++++-- truststores/extended.go | 6 ++ truststores/keyring.go | 148 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 181 insertions(+), 4 deletions(-) create mode 100644 truststores/keyring.go diff --git a/cmd/main.go b/cmd/main.go index 93ad9d5..d7f97f1 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -32,6 +32,7 @@ var ( } trustStoreDir string + trustStoreKeyring string noSpec string minimumSecurityLevel = 0 defaultSymmetricKeySize = 0 @@ -50,7 +51,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(&trustStoreDir, "tskeyring", "k", "", + "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", @@ -67,17 +71,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/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/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 +} From 93372c32194ad73a1ffe2e0847b68a5ad9c31331 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 8 Aug 2022 14:46:13 +0200 Subject: [PATCH 18/31] Increase re-key message limit --- session-wire.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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) ) From d4c5d7a4d416d118806170f80c6af8ceaf2a18b2 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 8 Aug 2022 14:46:20 +0200 Subject: [PATCH 19/31] Fix linter --- .golangci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.golangci.yml b/.golangci.yml index a1720fe..6c348ac 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -7,7 +7,6 @@ linters: - containedctx - contextcheck - cyclop - - errchkjson # Triggers stack overflow on github.com/safing/jess - exhaustivestruct - forbidigo - funlen @@ -32,6 +31,7 @@ linters: - whitespace - wrapcheck - wsl + - nolintlint linters-settings: revive: From 48163a56ce0e1666b125ca756f9c6376dce28921 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 8 Aug 2022 14:46:34 +0200 Subject: [PATCH 20/31] Add deps --- go.mod | 1 + go.sum | 8 ++++++++ 2 files changed, 9 insertions(+) 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= From f91ea4bc19f418449c8be2b11949059e7196fc07 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 8 Aug 2022 14:47:20 +0200 Subject: [PATCH 21/31] Add option to immediately export/backup generated signet --- cmd/cfg-signet.go | 46 +++++++++++++++++++++++--------------------- cmd/cmd-generate.go | 47 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 67 insertions(+), 26 deletions(-) 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-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 }, } ) From 127c0b4f8d04eb8519b3de2d438e41e4a9b5d96d Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Mon, 8 Aug 2022 14:47:35 +0200 Subject: [PATCH 22/31] Compile for arm64 --- pack | 6 ++++++ 1 file changed, 6 insertions(+) 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 { From 663e8fcfc77fa8ead02ac0708678b0a6deb6af67 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Fri, 12 Aug 2022 13:14:19 +0200 Subject: [PATCH 23/31] Build jess cli with correct binary name --- cmd/build | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cmd/build b/cmd/build index ab409b7..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/portbase/info" -go build -ldflags "-X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" "$@" +go build -o "${BIN_NAME}" -ldflags "-X ${BUILD_PATH}.commit=${BUILD_COMMIT} -X ${BUILD_PATH}.buildOptions=${BUILD_BUILDOPTIONS} -X ${BUILD_PATH}.buildUser=${BUILD_USER} -X ${BUILD_PATH}.buildHost=${BUILD_HOST} -X ${BUILD_PATH}.buildDate=${BUILD_DATE} -X ${BUILD_PATH}.buildSource=${BUILD_SOURCE}" "$@" From 3e3a8c29b4e525eee152a79060123aa4d5eefa7f Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Fri, 12 Aug 2022 13:15:58 +0200 Subject: [PATCH 24/31] Fix and improve recent additions --- cmd/cmd-verify.go | 8 ++++---- cmd/main.go | 2 +- envelope.go | 4 ++-- filesig/main.go | 3 +++ import_export.go | 6 ++++-- 5 files changed, 14 insertions(+), 9 deletions(-) diff --git a/cmd/cmd-verify.go b/cmd/cmd-verify.go index 1efc832..061dcf6 100644 --- a/cmd/cmd-verify.go +++ b/cmd/cmd-verify.go @@ -83,7 +83,7 @@ var verifyCmd = &cobra.Command{ } // Only verify if .sig or .letter. - if strings.HasSuffix(path, sigFileExtension) || + if strings.HasSuffix(path, filesig.Extension) || strings.HasSuffix(path, letterFileExtension) { if err := verify(path, true); err != nil { verificationFails++ @@ -135,11 +135,11 @@ func verify(filename string, bulkMode bool) error { signedBy, err = verifyLetter(filename, bulkMode) case strings.HasSuffix(filename, letterFileExtension): signedBy, err = verifyLetter(filename, bulkMode) - case strings.HasSuffix(filename, sigFileExtension): - filename = strings.TrimSuffix(filename, sigFileExtension) + case strings.HasSuffix(filename, filesig.Extension): + filename = strings.TrimSuffix(filename, filesig.Extension) fallthrough default: - signame = filename + sigFileExtension + signame = filename + filesig.Extension signedBy, err = verifySig(filename, signame, bulkMode) } diff --git a/cmd/main.go b/cmd/main.go index d7f97f1..b603063 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -53,7 +53,7 @@ func main() { rootCmd.PersistentFlags().StringVarP(&trustStoreDir, "tsdir", "d", "", "specify a truststore directory (default loaded from JESS_TS_DIR env variable)", ) - rootCmd.PersistentFlags().StringVarP(&trustStoreDir, "tskeyring", "k", "", + 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", "", diff --git a/envelope.go b/envelope.go index 2935041..71c2aa8 100644 --- a/envelope.go +++ b/envelope.go @@ -285,14 +285,14 @@ func (e *Envelope) CleanSignets() { } } for i, signet := range e.Senders { - e.Secrets[i] = &Signet{ + e.Senders[i] = &Signet{ Version: signet.Version, ID: signet.ID, Scheme: signet.Scheme, } } for i, signet := range e.Recipients { - e.Secrets[i] = &Signet{ + e.Recipients[i] = &Signet{ Version: signet.Version, ID: signet.ID, Scheme: signet.Scheme, diff --git a/filesig/main.go b/filesig/main.go index b4f54ba..b71ad87 100644 --- a/filesig/main.go +++ b/filesig/main.go @@ -9,6 +9,9 @@ import ( "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) diff --git a/import_export.go b/import_export.go index 347d5c2..86de909 100644 --- a/import_export.go +++ b/import_export.go @@ -182,11 +182,13 @@ func EnvelopeFromTextFormat(textFormat string) (*Envelope, error) { return EnvelopeFromBase58(splitted[len(splitted)-1]) } -var replaceForTextFormatMatcher = regexp.MustCompile(`[^A-Za-z\-]+`) +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( - replaceForTextFormatMatcher.ReplaceAllString(s, "_"), + strings.Trim( + replaceForTextFormatMatcher.ReplaceAllString(s, "-"), "-", + ), ) } From 12f5da337ce956c5f5f2c963a5f99025bdb6c162 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Fri, 12 Aug 2022 13:16:16 +0200 Subject: [PATCH 25/31] Add support for file signing suite in manager --- cmd/cfg-envelope.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cmd/cfg-envelope.go b/cmd/cfg-envelope.go index 873ac6b..48d2020 100644 --- a/cmd/cfg-envelope.go +++ b/cmd/cfg-envelope.go @@ -31,6 +31,7 @@ func newEnvelope(name string) (*jess.Envelope, error) { "Encrypt for someone and sign", "Encrypt for someone but don't sign", "Sign a file (wrapped)", + "Sign a file (separate sig)", }, } err := survey.AskOne(prompt, &preset, nil) @@ -54,8 +55,11 @@ 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": - envelope.SuiteID = jess.SuiteSignFileV1 + 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 { From 7566eefcd7d7f95ec62357e697421c0a7e98e62f Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Fri, 12 Aug 2022 13:16:43 +0200 Subject: [PATCH 26/31] Expose raw hasher and add raw hash equality function --- lhash/algs.go | 5 +++++ lhash/labeledhash.go | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/lhash/algs.go b/lhash/algs.go index 635555d..e4a15cc 100644 --- a/lhash/algs.go +++ b/lhash/algs.go @@ -127,6 +127,11 @@ func (a Algorithm) String() string { } } +// 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) diff --git a/lhash/labeledhash.go b/lhash/labeledhash.go index 58951cd..d0674b1 100644 --- a/lhash/labeledhash.go +++ b/lhash/labeledhash.go @@ -168,6 +168,13 @@ func (lh *LabeledHash) Equal(other *LabeledHash) bool { subtle.ConstantTimeCompare(lh.digest, other.digest) == 1 } +// 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 +} + // 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)) From 0fdd07b0e262746c564ead5487c3b965b67ffe54 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Wed, 28 Sep 2022 14:45:01 +0200 Subject: [PATCH 27/31] Improve labeled hash helpers --- lhash/labeledhash.go | 38 ++++++++++++++++++++++++++++++-------- lhash/labeledhash_test.go | 8 ++++---- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/lhash/labeledhash.go b/lhash/labeledhash.go index d0674b1..b395b5a 100644 --- a/lhash/labeledhash.go +++ b/lhash/labeledhash.go @@ -175,16 +175,38 @@ func (lh *LabeledHash) EqualRaw(otherDigest []byte) bool { return subtle.ConstantTimeCompare(lh.digest, otherDigest) == 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)) +// 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) { From 19f008e7013aeeaf02ac70380a5c5837caad2e58 Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Wed, 28 Sep 2022 14:45:08 +0200 Subject: [PATCH 28/31] Update golang-ci lint config --- .golangci.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.golangci.yml b/.golangci.yml index 6c348ac..8202fea 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,6 +8,7 @@ linters: - contextcheck - cyclop - exhaustivestruct + - exhaustruct - forbidigo - funlen - gochecknoglobals @@ -17,6 +18,7 @@ linters: - goerr113 - gomnd - ifshort + - interfacebloat - interfacer - ireturn - lll @@ -24,6 +26,9 @@ linters: - nilnil - nlreturn - noctx + - nolintlint + - nonamedreturns + - nosnakecase - revive - tagliatelle - testpackage @@ -31,7 +36,6 @@ linters: - whitespace - wrapcheck - wsl - - nolintlint linters-settings: revive: From 678c558e7f4d7d9e2b0b807417046b768d0bc71c Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Wed, 28 Sep 2022 19:56:41 +0200 Subject: [PATCH 29/31] Implement review suggestions and fix tests and linter errors --- cmd/cmd-close.go | 6 +++--- cmd/cmd-open.go | 5 ++--- cmd/cmd-verify.go | 6 +++--- cmd/main.go | 1 - core_test.go | 17 +++++++---------- filesig/helpers.go | 7 +++---- import_export.go | 1 + lhash/labeledhash.go | 4 ++-- truststores/io.go | 9 ++++----- 9 files changed, 25 insertions(+), 31 deletions(-) diff --git a/cmd/cmd-close.go b/cmd/cmd-close.go index 62e7e2f..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" @@ -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-open.go b/cmd/cmd-open.go index edf3ecf..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 diff --git a/cmd/cmd-verify.go b/cmd/cmd-verify.go index 061dcf6..bd0401f 100644 --- a/cmd/cmd-verify.go +++ b/cmd/cmd-verify.go @@ -3,8 +3,8 @@ package main import ( "errors" "fmt" + "io" "io/fs" - "io/ioutil" "os" "path/filepath" "strings" @@ -185,9 +185,9 @@ func verifyLetter(filename string, silent bool) (signedBy []string, err error) { // 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 nil, err diff --git a/cmd/main.go b/cmd/main.go index b603063..ef46179 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -16,7 +16,6 @@ import ( const ( stdInOutFilename = "-" letterFileExtension = ".letter" - sigFileExtension = ".sig" warnFileSize = 12000000 // 120MB ) diff --git a/core_test.go b/core_test.go index 045abd3..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 { @@ -404,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") } @@ -514,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/filesig/helpers.go b/filesig/helpers.go index 0037a9c..90e7918 100644 --- a/filesig/helpers.go +++ b/filesig/helpers.go @@ -3,7 +3,6 @@ package filesig import ( "errors" "fmt" - "io/ioutil" "os" "strings" @@ -51,7 +50,7 @@ func SignFile(dataFilePath, signatureFilePath string, metaData map[string]string return nil, fmt.Errorf("failed to sign file: %w", err) } - sigFileData, err := ioutil.ReadFile(signatureFilePath) + sigFileData, err := os.ReadFile(signatureFilePath) var newSigFileData []byte switch { case err == nil: @@ -71,7 +70,7 @@ func SignFile(dataFilePath, signatureFilePath string, metaData map[string]string } // Write the signature to file. - if err := ioutil.WriteFile(signatureFilePath, newSigFileData, 0o0644); err != nil { //nolint:gosec + if err := os.WriteFile(signatureFilePath, newSigFileData, 0o0644); err != nil { //nolint:gosec return nil, fmt.Errorf("failed to write signature to file: %w", err) } @@ -86,7 +85,7 @@ func VerifyFile(dataFilePath, signatureFilePath string, metaData map[string]stri var lastErr error // Read signature from file. - sigFileData, err := ioutil.ReadFile(signatureFilePath) + sigFileData, err := os.ReadFile(signatureFilePath) if err != nil { return nil, fmt.Errorf("failed to read signature file: %w", err) } diff --git a/import_export.go b/import_export.go index 86de909..84eccdd 100644 --- a/import_export.go +++ b/import_export.go @@ -7,6 +7,7 @@ import ( "strings" ) +// Keywords and Prefixes for the export text format. const ( ExportSenderKeyword = "sender" ExportSenderPrefix = "sender:" diff --git a/lhash/labeledhash.go b/lhash/labeledhash.go index b395b5a..68e5475 100644 --- a/lhash/labeledhash.go +++ b/lhash/labeledhash.go @@ -36,7 +36,7 @@ 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.OpenFile(pathToFile, os.O_RDONLY, 0) + file, err := os.Open(pathToFile) if err != nil { return nil, fmt.Errorf("failed to open file: %w", err) } @@ -181,7 +181,7 @@ func (lh *LabeledHash) Matches(data []byte) bool { } // MatchesData returns true if the digest of the given data matches the hash. -// DEPRECATED: Use Matches instead. +// Deprecated: Use Matches instead. func (lh *LabeledHash) MatchesData(data []byte) bool { return lh.Equal(Digest(lh.alg, data)) } 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 From 88e4c926337d0476c72bce87d287753ff0934f1f Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Wed, 28 Sep 2022 20:03:11 +0200 Subject: [PATCH 30/31] Add missing and ignored files --- .gitignore | 3 +- cmd/cmd-import-export.go | 170 +++++++++++++++++++++++++++++++++++++++ cmd/cmd-sign.go | 62 ++++++++++++++ 3 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 cmd/cmd-import-export.go create mode 100644 cmd/cmd-sign.go 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/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-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 + }, + } +) From 9ba2af21ac3f6674a28b8aaf505cee347d1226ac Mon Sep 17 00:00:00 2001 From: Daniel <dhaavi@users.noreply.github.com> Date: Wed, 28 Sep 2022 20:09:09 +0200 Subject: [PATCH 31/31] Update go workflow --- .github/workflows/go.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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