diff --git a/.gitignore b/.gitignore index 1926fb7..dccf69d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ cpu.out vendor +cmd/cmd +cmd/jess diff --git a/.golangci.yml b/.golangci.yml index 3c2d6b3..e314a4c 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -7,5 +7,10 @@ linters: - funlen - whitespace - wsl - - godox +linters-settings: + godox: + # report any comments starting with keywords, this is useful for TODO or FIXME comments that + # might be left in the code accidentally and should be resolved before merging + keywords: + - FIXME diff --git a/Gopkg.lock b/Gopkg.lock index 05214eb..6a76123 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -104,7 +104,7 @@ [[projects]] branch = "master" - digest = "1:23e3d5f66ff9fd60dd264fbf26e71cab6b4a3bd2ef993a1645432d5c4c3b50fc" + digest = "1:4df631634d7dc4496e9c51436cc2d3ccd0d0d4734640f63108950fe68b348b7e" name = "golang.org/x/crypto" packages = [ "blake2b", @@ -118,6 +118,7 @@ "poly1305", "salsa20", "salsa20/salsa", + "scrypt", "sha3", ] pruneopts = "UT" @@ -164,6 +165,7 @@ "golang.org/x/crypto/pbkdf2", "golang.org/x/crypto/poly1305", "golang.org/x/crypto/salsa20", + "golang.org/x/crypto/scrypt", "golang.org/x/crypto/sha3", ] solver-name = "gps-cdcl" diff --git a/README.md b/README.md index afdbe94..38acc3e 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,20 @@ Should any of these properties _not_ be required, the user has to intentionally There is some more detail in [SPEC.md](./SPEC.md). +### Known Issues + +#### Secure Key Deletion + +Go currently does not provide functionality to securely handle sensitive data, such as key material, in memory. Thus, it cannot be guaranteed that key material is correctly wiped from memory and won't leak when swapping memory to disk. + +There is an [issue in the Golang project](https://github.com/golang/go/issues/21865) about this. + +We evaluated two existing workarounds for this: +1) Using `reflect` to dive into the internals of all algorithms to (possibly) delete key material. This is used in the Go implementation of Wireguard, for example. This still does not guarantee that Go internally deletes the key material. +2) Use of the [Go memguard package](https://github.com/awnumar/memguard). While this would improve handling of key material that we directly manage, it will still not solve protecting all the intermediate values used in the implementations of the algorithms. + +We currently settled on waiting for further progress on the issue by the Go development team, and will reevaluate the progress regularly. + ### Testing Basically, tests are run like this: diff --git a/SPEC.md b/SPEC.md index a5dc7c5..98bdd36 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,5 +1,7 @@ # Jess Specification +This document takes a closer look at the inner workings of Jess. It does not, however, define the formats and some other aspects. First, some basics will be covered. Then, the wire protocol is given a closer look. + ## Basics The basic building blocks of jess are: @@ -146,3 +148,18 @@ c->c: "gets secret [s2] from [e1, c2], applies it to [client<—server] and [cli c->s: "sends APPLY flag" s->s: "applies secret [s2] to [client—>server]" --> + +### Limitation of the 0-RTT Wire Protocol Security + +The wire protocol is able to send data in the first message. Until the first roundtrip has been completed (client to server to client), there are weakened security guarantees: + +1) If the static key (semi-static 24h TTL key in case of SPN) is compromised, there are _lesser_ to _no_ security guarantees: +- A passive attacker, in possession of the private static key, is able to read all outgoing messages until the first roundtrip is completed. +- An active attacker is able to execute a Man-in-the-Middle attack and completely obliterate all security guarantees. + +2) Incoming messages (server to client) have a reduced guarantee of security until the first roundtrip is completed: +An active attacker is able to execute a Man-in-the-Middle attack, completely _replacing_ the first outgoing (client to server) message. The attacker can choose the contents of that message to his liking. The response from the server will then be readable by the attacker, but the client will discard the message as authentication of it will fail. +As the Wire Protocol does not do any form of client/sender authentication, this is a problem for protocols that use underlying information, such as the IP address, for authentication. The sub-protocol must always authenticate clients to their needs and do not expect any guarantees from the Wire Protocol in that regard. +Also, note that this is also the case with the protocols in the Noise framework that start with an "N". + +This wire protocol was specifically built for SPN and the implementation there is fully aware of all the limitations of the protocol. diff --git a/cmd/doc.go b/cmd/README.md similarity index 61% rename from cmd/doc.go rename to cmd/README.md index 46a7e4f..1146e1e 100644 --- a/cmd/doc.go +++ b/cmd/README.md @@ -1,9 +1,12 @@ -/* -CLI: +### Jess CLI +This is currently still more of a planning and working document. +Here is the CLI interface that is planned: + +``` jess create -jess close with +jess close with encrypt a file, write to file with the same name, but with a .letter suffix -o ... write output to @@ -23,11 +26,12 @@ jess show - letter - seal (signature-only letter) -global arguments ---tsdir /path/to/truststore ---seclevel ---symkeysize ---quiet only output errors and warnings +jess generate + generate a new signet and store both signet and recipient in the truststore -*/ -package main +global arguments + --tsdir /path/to/truststore + --seclevel + --symkeysize + --quiet only output errors and warnings +``` diff --git a/cmd/cfg-envelope.go b/cmd/cfg-envelope.go index e16a42a..6819292 100644 --- a/cmd/cfg-envelope.go +++ b/cmd/cfg-envelope.go @@ -26,10 +26,10 @@ func newEnvelope(name string) (*jess.Envelope, error) { Message: "Select preset:", Options: []string{ "Encrypt with password", - "Encrypt with keyfile", - "Encrypt for someone", + "Encrypt with key", + "Encrypt for someone and sign", + "Encrypt for someone but don't sign", "Sign a file", - "Custom from scratch", }, } err := survey.AskOne(prompt, &preset, nil) @@ -39,26 +39,23 @@ func newEnvelope(name string) (*jess.Envelope, error) { switch preset { case "Encrypt with password": - envelope.Tools = jess.RecommendedStoragePassword + envelope.SuiteID = jess.SuitePassword err = selectSignets(envelope, "pw") - - case "Encrypt with keyfile": - envelope.Tools = jess.RecommendedStorageKey + case "Encrypt with key": + envelope.SuiteID = jess.SuiteKey err = selectSignets(envelope, "key") - - case "Encrypt for someone": - envelope.Tools = jess.RecommendedStorageKey + case "Encrypt for someone and sign": + envelope.SuiteID = jess.SuiteComplete err = selectSignets(envelope, "recipient") if err == nil { err = selectSignets(envelope, "sender") } - + case "Encrypt for someone but don't sign": + envelope.SuiteID = jess.SuiteRcptOnly + err = selectSignets(envelope, "recipient") case "Sign a file": - envelope.NoConfidentiality().NoIntegrity().NoRecipientAuth() + envelope.SuiteID = jess.SuiteSign err = selectSignets(envelope, "sender") - - case "Custom from scratch": - // do nothing } if err != nil { return nil, err @@ -72,12 +69,13 @@ func editEnvelope(envelope *jess.Envelope) error { // main menu // print envelope status + envelope.SecurityLevel = 0 // reset session, err := envelope.Correspondence(trustStore) if err != nil { fmt.Printf("Envelope status: %s\n", err) } else { fmt.Println("Envelope status: valid.") - envelope.MinimumSecurityLevel = session.SecurityLevel + envelope.SecurityLevel = session.SecurityLevel } // sub menu @@ -86,17 +84,18 @@ func editEnvelope(envelope *jess.Envelope) error { Message: "Select to edit", Options: formatColumns([][]string{ {"Done", "save and return"}, - {" "}, - {"Requirements", formatRequirements(envelope)}, - {"Tools", strings.Join(envelope.Tools, ", ")}, + {""}, + {"Suite", envelope.SuiteID}, + {"", "provides " + formatRequirements(envelope.Suite().Provides)}, + {"", "and " + formatSecurityLevel(envelope.Suite().SecurityLevel)}, {"Secrets", formatSignetNames(envelope.Secrets)}, {"Recipients", formatSignetNames(envelope.Recipients)}, {"Senders", formatSignetNames(envelope.Senders)}, - {" "}, + {""}, {"Abort", "discard changes and return"}, {"Delete", "delete and return"}, }), - PageSize: 10, + PageSize: 15, } err = survey.AskOne(prompt, &submenu, nil) if err != nil { @@ -111,10 +110,8 @@ func editEnvelope(envelope *jess.Envelope) error { return nil case strings.HasPrefix(submenu, "Delete"): return trustStore.DeleteEnvelope(envelope.Name) - case strings.HasPrefix(submenu, "Requirements"): - err = editEnvelopeRequirements(envelope) - case strings.HasPrefix(submenu, "Tools"): - err = editEnvelopeTools(envelope) + case strings.HasPrefix(submenu, "Suite"): + err = editEnvelopeSuite(envelope) case strings.HasPrefix(submenu, "Secrets"): err = selectSignets(envelope, "pw/key") case strings.HasPrefix(submenu, "Recipients"): @@ -128,69 +125,30 @@ func editEnvelope(envelope *jess.Envelope) error { } } -func editEnvelopeRequirements(envelope *jess.Envelope) error { - // TODO: improve - - // get reqs - requirements := envelope.Requirements() - if requirements == nil { - return errors.New("envelope requirements uninitialized") +func editEnvelopeSuite(envelope *jess.Envelope) error { + all := jess.Suites() + suiteOptions := make([][]string, 0, len(all)) + for _, suite := range all { + suiteOptions = append(suiteOptions, []string{ + suite.ID, + "provides " + suite.Provides.ShortString(), + formatSecurityLevel(suite.SecurityLevel), + "uses " + strings.Join(suite.Tools, ", "), + formatSuiteStatus(suite), + }) } - // build defaults - var defaults []string - if requirements.Has(jess.Confidentiality) { - defaults = append(defaults, "Confidentiality") + var selectedSuite string + prompt := &survey.Select{ + Message: "Select suite", + Options: formatColumns(suiteOptions), + PageSize: 10, } - if requirements.Has(jess.Integrity) { - defaults = append(defaults, "Integrity") - } - if requirements.Has(jess.RecipientAuthentication) { - defaults = append(defaults, "Recipient Authentication") - } - if requirements.Has(jess.SenderAuthentication) { - defaults = append(defaults, "Sender Authentication") - } - - // prompt - var selected []string - prompt := &survey.MultiSelect{ - Message: "Select requirements:", - Options: []string{ - "Confidentiality", - "Integrity", - "Recipient Authentication", - "Sender Authentication", - }, - Default: defaults, - } - err := survey.AskOne(prompt, &selected, nil) + err := survey.AskOne(prompt, &selectedSuite, nil) if err != nil { return err } - // parse - requirements.Remove(jess.Confidentiality) - requirements.Remove(jess.Integrity) - requirements.Remove(jess.RecipientAuthentication) - requirements.Remove(jess.SenderAuthentication) - for _, req := range selected { - switch req { - case "Confidentiality": - requirements.Add(jess.Confidentiality) - case "Integrity": - requirements.Add(jess.Integrity) - case "Recipient Authentication": - requirements.Add(jess.RecipientAuthentication) - case "Sender Authentication": - requirements.Add(jess.SenderAuthentication) - } - } - - return nil -} - -func editEnvelopeTools(envelope *jess.Envelope) (err error) { - envelope.Tools, err = pickTools(envelope.Tools, "Select tools:") - return err + envelope.SuiteID = strings.Fields(selectedSuite)[0] + return envelope.ReloadSuite() } diff --git a/cmd/cfg-signet.go b/cmd/cfg-signet.go index cef217a..bf67988 100644 --- a/cmd/cfg-signet.go +++ b/cmd/cfg-signet.go @@ -2,7 +2,6 @@ package main import ( "errors" - "fmt" "strings" "time" @@ -32,19 +31,8 @@ func newSignet(name, scheme string) (*jess.Signet, error) { schemeSelection := [][]string{ {jess.SignetSchemePassword, "Password"}, {jess.SignetSchemeKey, "Key", "dynamic b/s (set manually via --symkeysize)"}, - } - for _, tool := range tools.AsList() { - // check if tool support Signets - switch tool.Info.Purpose { - case tools.PurposeKeyExchange, - tools.PurposeKeyEncapsulation, - tools.PurposeSigning: - schemeSelection = append(schemeSelection, []string{ - tool.Info.Name, - tool.Info.FormatPurpose(), - formatToolSecurityLevel(tool), - }) - } + {"ECDH-X25519", "Receiving (KeyExchange)"}, + {"Ed25519", "Signing"}, } // select scheme @@ -204,42 +192,6 @@ func selectSignets(envelope *jess.Envelope, scope string) error { return err } - // add schemes to envelope -checkSchemaLoop: - for _, signet := range selectedSignets { - switch signet.Scheme { - case jess.SignetSchemePassword, jess.SignetSchemeKey: - default: - for _, toolID := range envelope.Tools { - scheme := toolID - if strings.Contains(scheme, "(") { - scheme = strings.Split(scheme, "(")[0] - } - if scheme == signet.Scheme { - continue checkSchemaLoop - } - } - - // schema not found in envelope toolset - tool, err := signet.Tool() - if err != nil { - return err - } - if tool.Info.HasOption(tools.OptionNeedsManagedHasher) || - tool.Info.HasOption(tools.OptionNeedsDedicatedHasher) { - // add hash tool - hashToolName, err := pickHashTool(fmt.Sprintf("Select hash tool for %s:", tool.Info.Name), tool.Info.SecurityLevel) - if err != nil { - return err - } - envelope.Tools = append(envelope.Tools, fmt.Sprintf("%s(%s)", tool.Info.Name, hashToolName)) - } else { - envelope.Tools = append(envelope.Tools, signet.Scheme) - } - - } - } - // make stubs selectedSignetStubs := make([]*jess.Signet, 0, len(selectedSignets)) for _, signet := range selectedSignets { diff --git a/cmd/cfg-tools.go b/cmd/cfg-tools.go index 4267c25..d873ed5 100644 --- a/cmd/cfg-tools.go +++ b/cmd/cfg-tools.go @@ -11,7 +11,7 @@ import ( "github.com/AlecAivazis/survey" ) -func pickTools(toolNames []string, promptMsg string) ([]string, error) { +func pickTools(toolNames []string, promptMsg string) ([]string, error) { //nolint:unused,deadcode // TODO var toolSelection [][]string //nolint:prealloc preSelectedTools := make([]string, 0, len(toolNames)) var preSelected int @@ -101,7 +101,7 @@ func pickTools(toolNames []string, promptMsg string) ([]string, error) { return newTools, nil } -func pickHashTool(prompt string, minSecurityLevel int) (string, error) { +func pickHashTool(prompt string, minSecurityLevel int) (string, error) { //nolint:unused // TODO var hashToolSelection [][]string for _, hashTool := range hashtools.AsList() { if hashTool.SecurityLevel >= minSecurityLevel { @@ -126,7 +126,7 @@ func pickHashTool(prompt string, minSecurityLevel int) (string, error) { return strings.Fields(selectedEnty)[0], nil } -func stringInSlice(s string, a []string) bool { +func stringInSlice(s string, a []string) bool { //nolint:unused // TODO for _, entry := range a { if entry == s { return true diff --git a/cmd/cmd-close.go b/cmd/cmd-close.go index 766f80e..a97ca7e 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") + closeCmd.Flags().StringVarP(&closeFlagOutput, "output", "o", "", "specify output file (`-` for stdout") } var ( @@ -20,9 +20,11 @@ var ( closeCmdHelp = "usage: jess close with " closeCmd = &cobra.Command{ - Use: "close", - Short: "encrypt file", - PreRunE: requireTrustStore, + Use: "close with ", + Short: "encrypt file", + Long: "encrypt file with the given envelope. Use `-` to use stdin", + DisableFlagsInUseLine: true, + PreRunE: requireTrustStore, RunE: func(cmd *cobra.Command, args []string) error { registerPasswordCallbacks() diff --git a/cmd/cmd-configure.go b/cmd/cmd-configure.go index 8d2eec9..a1d03a8 100644 --- a/cmd/cmd-configure.go +++ b/cmd/cmd-configure.go @@ -16,10 +16,11 @@ func init() { var ( configureCmd = &cobra.Command{ - Use: "configure", - Short: "configure (and create) envelope", - Args: cobra.MaximumNArgs(1), - PreRunE: requireTrustStore, + Use: "configure ", + Short: "configure (and create) envelope", + DisableFlagsInUseLine: true, + Args: cobra.MaximumNArgs(1), + PreRunE: requireTrustStore, RunE: func(cmd *cobra.Command, args []string) (err error) { // check envelope name existence if len(args) == 0 { diff --git a/cmd/cmd-generate.go b/cmd/cmd-generate.go index cdf9d43..e4f9b21 100644 --- a/cmd/cmd-generate.go +++ b/cmd/cmd-generate.go @@ -15,10 +15,11 @@ var ( generateFlagScheme string generateCmd = &cobra.Command{ - Use: "generate", - Short: "generate a new signet", - Args: cobra.NoArgs, - PreRunE: requireTrustStore, + Use: "generate", + Short: "generate a new signet", + DisableFlagsInUseLine: true, + Args: cobra.NoArgs, + PreRunE: requireTrustStore, RunE: func(cmd *cobra.Command, args []string) error { _, err := newSignet(generateFlagName, generateFlagScheme) return err diff --git a/cmd/cmd-tools.go b/cmd/cmd-list.go similarity index 57% rename from cmd/cmd-tools.go rename to cmd/cmd-list.go index c24192b..f2c68f8 100644 --- a/cmd/cmd-tools.go +++ b/cmd/cmd-list.go @@ -2,22 +2,42 @@ package main import ( "fmt" + "strings" "github.com/spf13/cobra" + "github.com/safing/jess" "github.com/safing/jess/hashtools" "github.com/safing/jess/tools" ) func init() { - rootCmd.AddCommand(toolsCmd) + rootCmd.AddCommand(listCmd) } -var toolsCmd = &cobra.Command{ - Use: "tools", - Short: "list all available tools", +var listCmd = &cobra.Command{ + Use: "list", + Short: "list all available suites and tools", + DisableFlagsInUseLine: true, Run: func(cmd *cobra.Command, args []string) { - fmt.Printf("Tools\n\n") + fmt.Printf("Suites\n\n") + suitesTable := [][]string{ + {"Name/ID", "Provides", "Security Level", "Tools", "Notes"}, + } + for _, suite := range jess.Suites() { + suitesTable = append(suitesTable, []string{ + suite.ID, + suite.Provides.ShortString(), + formatSecurityLevel(suite.SecurityLevel), + strings.Join(suite.Tools, ", "), + formatSuiteStatus(suite), + }) + } + for _, line := range formatColumns(suitesTable) { + fmt.Println(line) + } + + fmt.Printf("\n\nTools\n\n") toolTable := [][]string{ {"Name/ID", "Purpose", "Security Level", "Author", "Comment"}, } @@ -34,7 +54,7 @@ var toolsCmd = &cobra.Command{ fmt.Println(line) } - fmt.Printf("\nHashTools\n\n") + fmt.Printf("\n\nHashTools\n\n") hashToolTable := [][]string{ {"Name/ID", "Security Level", "Author", "Comment"}, } diff --git a/cmd/cmd-manage.go b/cmd/cmd-manage.go index 5ac8e1f..deb232f 100644 --- a/cmd/cmd-manage.go +++ b/cmd/cmd-manage.go @@ -19,10 +19,11 @@ func init() { } var manageCmd = &cobra.Command{ - Use: "manage", - Short: "manage a trust store", - Args: cobra.MaximumNArgs(1), - PreRunE: requireTrustStore, + Use: "manage", + Short: "manage a trust store", + DisableFlagsInUseLine: true, + Args: cobra.MaximumNArgs(1), + PreRunE: requireTrustStore, RunE: func(cmd *cobra.Command, args []string) error { // select action var selectedAction string @@ -117,7 +118,11 @@ func manageEnvelopes() error { for _, envelope := range all { selection = append(selection, []string{ envelope.Name, - envelope.Requirements().ShortString(), + envelope.SuiteID, + fmt.Sprintf("provides %s and %s", + envelope.Suite().Provides.ShortString(), + formatSecurityLevel(envelope.Suite().SecurityLevel), + ), formatEnvelopeSignets(envelope), }) } diff --git a/cmd/cmd-open.go b/cmd/cmd-open.go index d78d216..97fedae 100644 --- a/cmd/cmd-open.go +++ b/cmd/cmd-open.go @@ -17,7 +17,7 @@ import ( func init() { rootCmd.AddCommand(openCmd) - openCmd.Flags().StringVarP(&openFlagOutput, "output", "o", "", "specify output file") + openCmd.Flags().StringVarP(&openFlagOutput, "output", "o", "", "specify output file (`-` for stdout") } var ( @@ -25,8 +25,9 @@ var ( openCmdHelp = "usage: jess open " openCmd = &cobra.Command{ - Use: "open", - Short: "decrypt a file", + Use: "open ", + Short: "decrypt file", + Long: "decrypt file with the given envelope. Use `-` to use stdin", RunE: func(cmd *cobra.Command, args []string) (err error) { registerPasswordCallbacks() @@ -39,7 +40,7 @@ var ( filename := args[0] outputFilename := openFlagOutput if outputFilename == "" { - if !strings.HasSuffix(filename, ".letter") || len(outputFilename) < 8 { + if !strings.HasSuffix(filename, ".letter") || len(filename) < 8 { return errors.New("cannot automatically derive output filename, please specify with --output") } outputFilename = strings.TrimSuffix(filename, ".letter") diff --git a/cmd/cmd-verify.go b/cmd/cmd-verify.go index 1aaf0a8..f53311d 100644 --- a/cmd/cmd-verify.go +++ b/cmd/cmd-verify.go @@ -21,8 +21,9 @@ var ( verifyCmdHelp = "usage: jess verify " verifyCmd = &cobra.Command{ - Use: "verify", - Short: "verify a file", + Use: "verify ", + Short: "verify file", + DisableFlagsInUseLine: true, RunE: func(cmd *cobra.Command, args []string) (err error) { // check args if len(args) != 1 { diff --git a/cmd/format.go b/cmd/format.go index f4adc10..d5b2b50 100644 --- a/cmd/format.go +++ b/cmd/format.go @@ -38,6 +38,10 @@ func formatColumns(table [][]string) []string { return lines } +func formatSecurityLevel(securityLevel int) string { + return fmt.Sprintf("%d b/s", securityLevel) +} + func formatToolSecurityLevel(tool *tools.Tool) string { if tool.Info.HasOption(tools.OptionNeedsSecurityLevel) { return "dynamic b/s (set manually via --seclevel)" @@ -45,7 +49,7 @@ func formatToolSecurityLevel(tool *tools.Tool) string { if tool.Info.SecurityLevel == 0 { return "" } - return fmt.Sprintf("%d b/s", tool.Info.SecurityLevel) + return formatSecurityLevel(tool.Info.SecurityLevel) } func formatSignetName(signet *jess.Signet) string { @@ -116,12 +120,11 @@ func formatSignetSecurityLevel(signet *jess.Signet) string { return fmt.Sprintf("%d b/s", securityLevel) } -func formatRequirements(envelope *jess.Envelope) string { - attrs := envelope.Requirements() - if attrs == nil || attrs.Empty() { +func formatRequirements(reqs *jess.Requirements) string { + if reqs == nil || reqs.Empty() { return "none (unsafe)" } - return attrs.String() + return reqs.String() } func formatSignetNames(signets []*jess.Signet) string { @@ -145,3 +148,14 @@ func formatEnvelopeSignets(envelope *jess.Envelope) string { } return strings.Join(sections, ", ") } + +func formatSuiteStatus(suite *jess.Suite) string { + switch suite.Status { + case jess.SuiteStatusDeprecated: + return "DEPRECATED" + case jess.SuiteStatusRecommended: + return "recommended" + default: + return "" + } +} diff --git a/core-wire.go b/core-wire.go index 453b579..ddfd285 100644 --- a/core-wire.go +++ b/core-wire.go @@ -418,7 +418,7 @@ func (w *WireSession) unwrapKeys(keyMaterial [][]byte) ([][]byte, error) { return keyMaterial, nil } -// burnEphemeralKeys burns all the ephemeral key material in the session. +// burnEphemeralKeys burns all the ephemeral key material in the session. This is currently ineffective, see known issues in the project's README. func (w *WireSession) burnEphemeralKeys() error { var lastErr error diff --git a/core-wire_test.go b/core-wire_test.go index 115cf5e..698a84b 100644 --- a/core-wire_test.go +++ b/core-wire_test.go @@ -11,20 +11,19 @@ import ( func TestWire(t *testing.T) { wireReKeyAfterMsgs = 100 - testWireCorrespondence(t, RecommendedNetwork, testData1) - testWireCorrespondence(t, RecommendedNetwork, testData2) + // current suites recommendation + testWireCorrespondence(t, getSuite(t, SuiteWire), testData1) + testWireCorrespondence(t, getSuite(t, SuiteWire), testData2) - testWireCorrespondence(t, []string{"ECDH-P224", "HKDF(SHA2-256)", "CHACHA20-POLY1305"}, testData1) - testWireCorrespondence(t, []string{"ECDH-P256", "HKDF(SHA2-256)", "CHACHA20-POLY1305"}, testData1) - testWireCorrespondence(t, []string{"ECDH-P384", "HKDF(SHA2-256)", "CHACHA20-POLY1305"}, testData1) - testWireCorrespondence(t, []string{"ECDH-P521", "HKDF(SHA2-256)", "CHACHA20-POLY1305"}, testData1) - testWireCorrespondence(t, []string{"RSA-OAEP(SHA2-256)", "HKDF(SHA2-256)", "CHACHA20-POLY1305"}, testData1) + // older suites + // testWireCorrespondence(t, getSuite(t, SuiteWireV1), testData1) + // testWireCorrespondence(t, getSuite(t, SuiteWireV1), testData2) } -func testWireCorrespondence(t *testing.T, toolIDs []string, testData string) { +func testWireCorrespondence(t *testing.T, suite *Suite, testData string) { wtr := &wireTestRange{t: t} - wtr.init(toolIDs, testData) - fmt.Printf("\n\nsimulating %v\n", toolIDs) + wtr.init(suite, testData) + fmt.Printf("\n\nsimulating %v\n", suite.ID) fmt.Println("two dots are one packet send+recv:") fmt.Println("\nclient ->") @@ -74,7 +73,7 @@ func testWireCorrespondence(t *testing.T, toolIDs []string, testData string) { duration := wtr.endTime.Sub(wtr.startTime) t.Logf( "%v tested: msgsize=%d, rekey every %d msgs, %d msgs, %d bytes, +%f%% overhead, %s, %s per msg, %f Mbit/s", - wtr.toolIDs, + wtr.suite.ID, len(testData), wireReKeyAfterMsgs, wtr.msgsTransferred, @@ -92,7 +91,7 @@ func testWireCorrespondence(t *testing.T, toolIDs []string, testData string) { type wireTestRange struct { t *testing.T - toolIDs []string + suite *Suite testData string client *Session @@ -108,12 +107,12 @@ type wireTestRange struct { endTime time.Time } -func (wtr *wireTestRange) init(toolIDs []string, testData string) (detectedInvalid bool) { - wtr.toolIDs = toolIDs +func (wtr *wireTestRange) init(suite *Suite, testData string) (detectedInvalid bool) { + wtr.suite = suite - e, err := setupEnvelopeAndTrustStore(wtr.t, wtr.toolIDs) + e, err := setupEnvelopeAndTrustStore(wtr.t, wtr.suite) if err != nil { - wtr.t.Fatalf("%v failed to setup envelope: %s", wtr.toolIDs, err) + wtr.t.Fatalf("%s failed to setup envelope: %s", wtr.suite.ID, err) return false } if e == nil { @@ -122,7 +121,7 @@ func (wtr *wireTestRange) init(toolIDs []string, testData string) (detectedInval wtr.client, err = e.WireCorrespondence(testTrustStore) if err != nil { - wtr.t.Fatalf("%v failed to init client session: %s", wtr.toolIDs, err) + wtr.t.Fatalf("%s failed to init client session: %s", wtr.suite.ID, err) } // setup and reset @@ -142,18 +141,18 @@ func (wtr *wireTestRange) init(toolIDs []string, testData string) (detectedInval func (wtr *wireTestRange) clientSend() { letter, err := wtr.client.Close([]byte(wtr.testData)) if err != nil { - wtr.t.Fatalf("%v failed to close: %s", wtr.toolIDs, err) + wtr.t.Fatalf("%s failed to close: %s", wtr.suite.ID, err) } wireData, err := letter.ToWire() if err != nil { - wtr.t.Fatalf("%v failed to serialize to wire: %s", wtr.toolIDs, err) + wtr.t.Fatalf("%s failed to serialize to wire: %s", wtr.suite.ID, err) } select { case wtr.clientToServer <- wireData: default: - wtr.t.Fatalf("%v could not send to server", wtr.toolIDs) + wtr.t.Fatalf("%s could not send to server", wtr.suite.ID) } fmt.Print(".") @@ -167,27 +166,27 @@ func (wtr *wireTestRange) serverRecv() { letter, err := LetterFromWire(wireData) if err != nil { - wtr.t.Fatalf("%v failed to parse initial wired letter: %s", wtr.toolIDs, err) + wtr.t.Fatalf("%s failed to parse initial wired letter: %s", wtr.suite.ID, err) } if wtr.server == nil { wtr.server, err = letter.WireCorrespondence(testTrustStore) if err != nil { - wtr.t.Fatalf("%v failed to init server session: %s", wtr.toolIDs, err) + wtr.t.Fatalf("%s failed to init server session: %s", wtr.suite.ID, err) } } origData, err := wtr.server.Open(letter) if err != nil { - wtr.t.Fatalf("%v failed to open: %s", wtr.toolIDs, err) + wtr.t.Fatalf("%s failed to open: %s", wtr.suite.ID, err) } wtr.bytesTransferred += len(origData) if string(origData) != wtr.testData { - wtr.t.Fatalf("%v testdata mismatch", wtr.toolIDs) + wtr.t.Fatalf("%s testdata mismatch", wtr.suite.ID) } default: - wtr.t.Fatalf("%v could not recv from client", wtr.toolIDs) + wtr.t.Fatalf("%s could not recv from client", wtr.suite.ID) } fmt.Print(".") @@ -196,18 +195,18 @@ func (wtr *wireTestRange) serverRecv() { func (wtr *wireTestRange) serverSend() { letter, err := wtr.server.Close([]byte(wtr.testData)) if err != nil { - wtr.t.Fatalf("%v failed to close: %s", wtr.toolIDs, err) + wtr.t.Fatalf("%s failed to close: %s", wtr.suite.ID, err) } wireData, err := letter.ToWire() if err != nil { - wtr.t.Fatalf("%v failed to serialize to wire: %s", wtr.toolIDs, err) + wtr.t.Fatalf("%s failed to serialize to wire: %s", wtr.suite.ID, err) } select { case wtr.serverToClient <- wireData: default: - wtr.t.Fatalf("%v could not send to client", wtr.toolIDs) + wtr.t.Fatalf("%s could not send to client", wtr.suite.ID) } fmt.Print(".") @@ -221,20 +220,20 @@ func (wtr *wireTestRange) clientRecv() { letter, err := LetterFromWire(wireData) if err != nil { - wtr.t.Fatalf("%v failed to parse initial wired letter: %s", wtr.toolIDs, err) + wtr.t.Fatalf("%s failed to parse initial wired letter: %s", wtr.suite.ID, err) } origData, err := wtr.client.Open(letter) if err != nil { - wtr.t.Fatalf("%v failed to open: %s", wtr.toolIDs, err) + wtr.t.Fatalf("%s failed to open: %s", wtr.suite.ID, err) } wtr.bytesTransferred += len(origData) if string(origData) != wtr.testData { - wtr.t.Fatalf("%v testdata mismatch", wtr.toolIDs) + wtr.t.Fatalf("%s testdata mismatch", wtr.suite.ID) } default: - wtr.t.Fatalf("%v could not recv from server", wtr.toolIDs) + wtr.t.Fatalf("%s could not recv from server", wtr.suite.ID) } fmt.Print(".") diff --git a/core.go b/core.go index 09f7420..2b4e33d 100644 --- a/core.go +++ b/core.go @@ -16,7 +16,7 @@ func (s *Session) Close(data []byte) (*Letter, error) { //nolint:gocognit if s.wire == nil || s.wire.msgNo == 0 { letter.Version = 1 - letter.Tools = s.envelope.Tools + letter.SuiteID = s.envelope.SuiteID } ///////////////// @@ -314,6 +314,15 @@ func (s *Session) Open(letter *Letter) ([]byte, error) { //nolint:gocognit,gocyc // Verify verifies signatures of the given letter. func (s *Session) Verify(letter *Letter) error { + + // debugging: + /* + fmt.Printf("opening: %+v\n", letter) + for _, sig := range letter.Signatures { + fmt.Printf("sig: %+v\n", sig) + } + */ + var err error if s.wire == nil && letter.Version != 1 { return fmt.Errorf("unsupported letter version: %d", letter.Version) @@ -323,44 +332,54 @@ func (s *Session) Verify(letter *Letter) error { // verify ///////// - if len(s.signers) == 0 { - return errors.New("letter is not signed") - } + // TODO: signature verification is run before tool setup. Currently, this is ok, but might change in the future. This might break additional signing algorithms that actually need setup. data := letter.Data - associatedSigningData := letter.compileAssociatedSigningData(nil) - // run managed signing hashers - if s.managedSigningHashers != nil { - err = s.feedManagedHashers(s.managedSigningHashers, data, associatedSigningData) - if err != nil { - return err - } - - defer s.resetManagedHashers(s.managedSigningHashers) + // build associated data + var associatedData []byte + if len(s.integratedCiphers) > 0 || len(s.macs) > 0 { + associatedData = letter.compileAssociatedData() } - // run signers - if len(s.envelope.Senders) != len(letter.Signatures) { - return errors.New("mismatch regarding available signatures and senders") - } - sigIndex := 0 + // Signature + if len(s.signers) > 0 { + associatedSigningData := letter.compileAssociatedSigningData(associatedData) - for _, tool := range s.signers { - //nolint:scopelint // function is executed immediately within loop - err = s.envelope.LoopSenders(tool.Info().Name, func(signet *Signet) error { - - err := tool.Verify(data, associatedSigningData, letter.Signatures[sigIndex].Value, signet) + // run managed signing hashers + if s.managedSigningHashers != nil { + err = s.feedManagedHashers(s.managedSigningHashers, data, associatedSigningData) if err != nil { - return fmt.Errorf("failed to verify signature (%s) with ID %s: %s", tool.Info().Name, letter.Signatures[sigIndex].ID, err) + return err } - sigIndex++ - return nil - }) - if err != nil { - return err + defer s.resetManagedHashers(s.managedSigningHashers) } + + // run signers + if len(s.envelope.Senders) != len(letter.Signatures) { + return errors.New("mismatch regarding available signatures and senders") + } + sigIndex := 0 + + for _, tool := range s.signers { + //nolint:scopelint // function is executed immediately within loop + err = s.envelope.LoopSenders(tool.Info().Name, func(signet *Signet) error { + + err := tool.Verify(data, associatedSigningData, letter.Signatures[sigIndex].Value, signet) + if err != nil { + return fmt.Errorf("failed to verify signature (%s) with ID %s: %s", tool.Info().Name, letter.Signatures[sigIndex].ID, err) + } + + sigIndex++ + return nil + }) + if err != nil { + return err + } + } + } else { + return errors.New("no signatures to verify") } return nil diff --git a/core_test.go b/core_test.go index 57f58a0..cef65b8 100644 --- a/core_test.go +++ b/core_test.go @@ -120,16 +120,8 @@ func init() { } func TestCoreBasic(t *testing.T) { - // toolsets to test - toolsets := [][]string{ - RecommendedStorageKey, - RecommendedStoragePassword, - {"HKDF(SHA2-256)", "CHACHA20-POLY1305"}, - {"PBKDF2-SHA2-256", "HKDF(SHA2-256)", "CHACHA20-POLY1305"}, - } - - for _, toolIDs := range toolsets { - testStorage(t, toolIDs) + for _, suite := range Suites() { + testStorage(t, suite) } } @@ -137,10 +129,12 @@ func TestCoreBasic(t *testing.T) { func TestCoreAllCombinations(t *testing.T) { // This shall test all tools in all combinations and every tool should be tested when placed before and after every other tool. - // skip in short tests - if testing.Short() { + // skip in short tests and when not running comprehensive + if testing.Short() || !runComprehensiveTestsActive { return } + // run this test with + // go test -timeout 10m github.com/safing/jess -v -count=1 -ldflags "-X github.com/safing/jess.RunComprehensiveTests=true" -run ^TestCoreAllCombinations$ // add all tools var all []string @@ -193,7 +187,7 @@ func TestCoreAllCombinations(t *testing.T) { // rotate to test before/after differences for i := 0; i < len(testTools); i++ { - detectedInvalid := testStorage(t, testTools) + detectedInvalid := testStorage(t, &Suite{Tools: testTools}) combinationsTested++ if detectedInvalid { combinationsDetectedInvalid++ @@ -207,7 +201,7 @@ func TestCoreAllCombinations(t *testing.T) { } } else { // test this order only - detectedInvalid := testStorage(t, testTools) + detectedInvalid := testStorage(t, &Suite{Tools: testTools}) combinationsTested++ if detectedInvalid { combinationsDetectedInvalid++ @@ -226,12 +220,12 @@ func TestCoreAllCombinations(t *testing.T) { t.Logf("of these, %d were successfully detected as invalid", combinationsDetectedInvalid) } -func testStorage(t *testing.T, toolIDs []string) (detectedInvalid bool) { - // t.Logf("testing storage with %v", toolIDs) +func testStorage(t *testing.T, suite *Suite) (detectedInvalid bool) { + // t.Logf("testing storage with %s", suite.ID) - e, err := setupEnvelopeAndTrustStore(t, toolIDs) + e, err := setupEnvelopeAndTrustStore(t, suite) if err != nil { - tErrorf(t, "%v failed: %s", toolIDs, err) + tErrorf(t, "%s failed: %s", suite.ID, err) return false } if e == nil { @@ -242,122 +236,72 @@ func testStorage(t *testing.T, toolIDs []string) (detectedInvalid bool) { s, err := e.Correspondence(testTrustStore) if err != nil { - tErrorf(t, "%v failed to init session (1): %s", toolIDs, err) + tErrorf(t, "%s failed to init session (1): %s", suite.ID, err) return false } letter, err := s.Close([]byte(testData1)) if err != nil { - tErrorf(t, "%v failed to close (1): %s", toolIDs, err) + tErrorf(t, "%s failed to close (1): %s", suite.ID, err) return false } msg, err := letter.ToJSON() if err != nil { - tErrorf(t, "%v failed to json encode (1): %s", toolIDs, err) + tErrorf(t, "%s failed to json encode (1): %s", suite.ID, err) return false } - // test 2: open from session + // test 2: open letter2, err := LetterFromJSON(msg) if err != nil { - tErrorf(t, "%v failed to json decode (2): %s", toolIDs, err) + tErrorf(t, "%s failed to json decode (2): %s", suite.ID, err) return false } - origData2, err := s.Open(letter2) + origData2, err := letter2.Open(e.suite.Provides, testTrustStore) if err != nil { - tErrorf(t, "%v failed to open (2): %s", toolIDs, err) + tErrorf(t, "%s failed to open (2): %s", suite.ID, err) return false } if string(origData2) != testData1 { - tErrorf(t, "%v original data mismatch (2): %s", toolIDs, string(origData2)) + tErrorf(t, "%s original data mismatch (2): %s", suite.ID, string(origData2)) return false } - if len(letter2.Signatures) > 0 { - err = s.Verify(letter2) - if err != nil { - tErrorf(t, "%v failed to verify (2): %s", toolIDs, err) - return false - } - } - - // extended tests - // only run for toolsets greater than 3 if we comprehensive testing is on - // for these tests, it is enough if every tool is tested once - if len(toolIDs) > 3 && RunComprehensiveTests != "true" { - return false - } - - // test 2.1: open again to check if reset after opening works + // test 2.1: verify letter21, err := LetterFromJSON(msg) if err != nil { - tErrorf(t, "%v failed to json decode (2.1): %s", toolIDs, err) + tErrorf(t, "%s failed to json decode (2): %s", suite.ID, err) return false } - origData21, err := s.Open(letter21) - if err != nil { - tErrorf(t, "%v failed to open (2.1): %s", toolIDs, err) - return false - } - if string(origData21) != testData1 { - tErrorf(t, "%v original data mismatch (2.1): %s", toolIDs, string(origData21)) - return false - } - - // test 2.2: close and open again to check if reset after closing works - - letter22, err := s.Close([]byte(testData1)) - if err != nil { - tErrorf(t, "%v failed to close (2.2): %s", toolIDs, err) - return false - } - - origData22, err := s.Open(letter22) - if err != nil { - tErrorf(t, "%v failed to open (2.2): %s", toolIDs, err) - return false - } - if string(origData22) != testData1 { - tErrorf(t, "%v original data mismatch (2.2): %s", toolIDs, string(origData22)) - return false - } - - // test 3: open from letter - - // FIXME - other improvements broke these tests, pausing them - /* - letter3, err := LetterFromJSON(msg) + if len(letter21.Signatures) > 0 { + err = letter21.Verify(e.suite.Provides, testTrustStore) if err != nil { - tErrorf(t, "%v failed to json decode (3): %s", toolIDs, err) + tErrorf(t, "%s failed to verify (2): %s", suite.ID, err) return false } - - origData3, err := letter3.Open(nil, testTrustStore) - if err != nil { - tErrorf(t, "%v failed to open (3): %s", toolIDs, err) - return false - } - if string(origData3) != testData1 { - tErrorf(t, "%v original data mismatch (3): %s", toolIDs, string(origData3)) - return false - } - */ + } return false } //nolint:gocognit,gocyclo -func setupEnvelopeAndTrustStore(t *testing.T, toolIDs []string) (*Envelope, error) { +func setupEnvelopeAndTrustStore(t *testing.T, suite *Suite) (*Envelope, error) { + // check if suite is registered + if suite.ID == "" { + // register as test suite + suite.ID = fmt.Sprintf("__unit_test_suite__" + strings.Join(suite.Tools, "__")) + registerSuite(suite) + } // create envelope baseline e := &Envelope{ - Tools: toolIDs, - requirements: newEmptyRequirements(), + SuiteID: suite.ID, + suite: suite, } // check vars @@ -366,7 +310,7 @@ func setupEnvelopeAndTrustStore(t *testing.T, toolIDs []string) (*Envelope, erro asyncKeyEstablishmentPresent := false // process tools and setup envelope - for _, toolID := range e.Tools { + for _, toolID := range e.suite.Tools { // remove hasher argument for now if strings.Contains(toolID, "(") { @@ -389,7 +333,7 @@ func setupEnvelopeAndTrustStore(t *testing.T, toolIDs []string) (*Envelope, erro e.Secrets = append(e.Secrets, pw) // add a second one! - if len(toolIDs) <= 2 { + if len(suite.Tools) <= 2 { pw1, err := getOrMakeSignet(t, nil, false, "test-pw-2") if err != nil { return nil, err @@ -422,66 +366,69 @@ func setupEnvelopeAndTrustStore(t *testing.T, toolIDs []string) (*Envelope, erro passDerPresent = true // add passderivation requirements later, as it is a bit special case tools.PurposeKeyExchange: - e.requirements.Add(RecipientAuthentication) + e.suite.Provides.Add(RecipientAuthentication) case tools.PurposeKeyEncapsulation: - e.requirements.Add(RecipientAuthentication) + e.suite.Provides.Add(RecipientAuthentication) case tools.PurposeSigning: - e.requirements.Add(SenderAuthentication) + e.suite.Provides.Add(SenderAuthentication) case tools.PurposeIntegratedCipher: - e.requirements.Add(Confidentiality) - e.requirements.Add(Integrity) + e.suite.Provides.Add(Confidentiality) + e.suite.Provides.Add(Integrity) case tools.PurposeCipher: - e.requirements.Add(Confidentiality) + e.suite.Provides.Add(Confidentiality) case tools.PurposeMAC: - e.requirements.Add(Integrity) + e.suite.Provides.Add(Integrity) } } // if invalid: test if toolset is recognized as invalid // no requirements -> only "meta" tools (kdf, pass derivation) - if e.requirements.Empty() { + if e.suite.Provides.Empty() { return nil, testInvalidToolset(e, "there are only meta tools in toolset") } // recipient auth, but no confidentiality? nope. - if e.requirements.Has(RecipientAuthentication) && - !e.requirements.Has(Confidentiality) { + if e.suite.Provides.Has(RecipientAuthentication) && + !e.suite.Provides.Has(Confidentiality) { return nil, testInvalidToolset(e, "authenticating the recipient without using confidentiality does not make sense") } // check if we are missing key derivation - this is only ok if we are merely signing if !keyDerPresent && - (len(e.requirements.all) != 1 || - !e.requirements.Has(SenderAuthentication)) { + (len(e.suite.Provides.all) != 1 || + !e.suite.Provides.Has(SenderAuthentication)) { return nil, testInvalidToolset(e, "omitting a key derivation tool is only allowed when merely signing") } // check if we have key derivation, but not need it if keyDerPresent && - (!e.requirements.Has(Confidentiality) && - !e.requirements.Has(Integrity)) { + (!e.suite.Provides.Has(Confidentiality) && + !e.suite.Provides.Has(Integrity)) { return nil, testInvalidToolset(e, "a key derivation tool was specified, albeit none is needed") } // add passderivation here, as to easier handle the other cases if passDerPresent { - e.requirements.Add(SenderAuthentication) - e.requirements.Add(RecipientAuthentication) + e.suite.Provides.Add(SenderAuthentication) + e.suite.Provides.Add(RecipientAuthentication) // need Confidentiality for this to make sense - if !e.requirements.Has(Confidentiality) { + if !e.suite.Provides.Has(Confidentiality) { return nil, testInvalidToolset(e, "using a password without confidentiality does not make sense") } } - if e.requirements.Has(Confidentiality) && - !e.requirements.Has(Integrity) { + if e.suite.Provides.Has(Confidentiality) && + !e.suite.Provides.Has(Integrity) { return nil, testInvalidToolset(e, "having confidentiality without integrity does not make sense") } // add static key if needed if !asyncKeyEstablishmentPresent && !passDerPresent && keyDerPresent { + e.suite.Provides.Add(SenderAuthentication) + e.suite.Provides.Add(RecipientAuthentication) + key, err := getOrMakeSignet(t, nil, false, "test-key-1") if err != nil { return nil, err @@ -489,7 +436,7 @@ func setupEnvelopeAndTrustStore(t *testing.T, toolIDs []string) (*Envelope, erro e.Secrets = append(e.Secrets, key) // add a second one! - if len(toolIDs) <= 2 { + if len(suite.Tools) <= 2 { key2, err := getOrMakeSignet(t, nil, false, "test-key-2") if err != nil { return nil, err diff --git a/defaults.go b/defaults.go index 752627b..baba152 100644 --- a/defaults.go +++ b/defaults.go @@ -3,10 +3,10 @@ package jess var ( // must be var in order decrease for testing for better speed - defaultSecurityLevel = 256 + defaultSecurityLevel = 128 minimumSecurityLevel = 0 - defaultSymmetricKeySize = 32 + defaultSymmetricKeySize = 16 minimumSymmetricKeySize = 0 ) diff --git a/envelope.go b/envelope.go index 056d6c8..e568b39 100644 --- a/envelope.go +++ b/envelope.go @@ -9,7 +9,8 @@ import ( type Envelope struct { //nolint:maligned // TODO Version uint8 Name string - Tools []string + SuiteID string + suite *Suite // Secret keys and passwords Secrets []*Signet @@ -26,11 +27,10 @@ type Envelope struct { //nolint:maligned // TODO // For users, envelopes describe how a letter is closed. // Therefore Secrets and Senders always refer to private keys and Recipients to public keys in that context. - // These distictions are important in order for the user to easily and confidently distinguish what is going to happen. Think of it as "human security". + // These distinctions are important in order for the user to easily and confidently distinguish what is going to happen. Think of it as "human security". - MinimumSecurityLevel int - No string - requirements *Requirements + // SecurityLevel is the security level of the envelope when it was created + SecurityLevel int // flag to signify if envelope is used for opening opening bool @@ -39,10 +39,8 @@ type Envelope struct { //nolint:maligned // TODO // NewUnconfiguredEnvelope returns an unconfigured, but slightly initialized envelope. func NewUnconfiguredEnvelope() *Envelope { e := &Envelope{ - Version: 1, - requirements: NewRequirements(), + Version: 1, } - e.SerializeRequirements() return e } @@ -52,14 +50,17 @@ func (e *Envelope) Correspondence(trustStore TrustStore) (*Session, error) { } func (e *Envelope) initCorrespondence(trustStore TrustStore, verifying bool) (*Session, error) { - err := e.LoadRequirements() + err := e.LoadSuite() if err != nil { return nil, err } + //nolint:gocritic // TODO: see below if verifying { - // prep sender signets only - err = e.prepSignets(e.Senders, e.opening, trustStore) + // TODO: prep sender signets only + // TODO: for this to work, newSession needs to only check verification related things + // err = e.prepSignets(e.Senders, e.opening, trustStore) + err = e.PrepareSignets(trustStore) } else { // prep all signets err = e.PrepareSignets(trustStore) @@ -92,82 +93,27 @@ func (e *Envelope) Check(trustStore TrustStore) error { return err } -// NoRecipientAuth removes the requirement to authenticate the recipient. -func (e *Envelope) NoRecipientAuth() *Envelope { - if e.requirements == nil { - e.requirements = NewRequirements() - } - - e.requirements.Remove(RecipientAuthentication) - return e +// Suite returns the loaded suite. +func (e *Envelope) Suite() *Suite { + return e.suite } -// NoSenderAuth removes the requirement to authenticate the sender. -func (e *Envelope) NoSenderAuth() *Envelope { - if e.requirements == nil { - e.requirements = NewRequirements() - } - - e.requirements.Remove(SenderAuthentication) - e.SerializeRequirements() - return e -} - -// NoConfidentiality removes the requirement to provide confidentiality. -func (e *Envelope) NoConfidentiality() *Envelope { - if e.requirements == nil { - e.requirements = NewRequirements() - } - - e.requirements.Remove(Confidentiality) - e.SerializeRequirements() - return e -} - -// NoIntegrity removes the requirement to provide integrity. -func (e *Envelope) NoIntegrity() *Envelope { - if e.requirements == nil { - e.requirements = NewRequirements() - } - - e.requirements.Remove(Integrity) - e.SerializeRequirements() - return e -} - -// Unsafe removes all requirements. -func (e *Envelope) Unsafe() *Envelope { - e.requirements = &Requirements{} - e.SerializeRequirements() - return e -} - -// Requirements returns the required requirements. -func (e *Envelope) Requirements() *Requirements { - return e.requirements -} - -// SetRequirements sets new requirements. -func (e *Envelope) SetRequirements(requirements *Requirements) { - e.requirements = requirements -} - -// LoadRequirements loads the required requirements from the struct's exposed negated "No" specification. -func (e *Envelope) LoadRequirements() error { - if e.requirements == nil { - attrs, err := ParseRequirementsFromNoSpec(e.No) - if err != nil { - return nil +// LoadSuite loads the suite specified in the envelope. +func (e *Envelope) LoadSuite() error { + if e.suite == nil { + suite, ok := GetSuite(e.SuiteID) + if !ok { + return fmt.Errorf("suite %s does not exist", e.SuiteID) } - - e.requirements = attrs + e.suite = suite } return nil } -// SerializeRequirements saves the requirement requirements in the struct's exposed negated "No" specification. -func (e *Envelope) SerializeRequirements() { - e.No = e.requirements.SerializeToNoSpec() +// ReloadSuite forces reloading the suite specified in the envelope. +func (e *Envelope) ReloadSuite() error { + e.suite = nil + return e.LoadSuite() } // LoopSecrets loops over all secrets of the given scheme. @@ -233,12 +179,16 @@ func (e *Envelope) prepSignets(signets []*Signet, recipients bool, storage Trust // load from storage if len(signet.Key) == 0 { if signet.Scheme == SignetSchemePassword { - err := fillPassword(signet, !recipients, storage, e.MinimumSecurityLevel) + err := fillPassword(signet, !recipients, storage, e.suite.SecurityLevel) if err != nil { return fmt.Errorf(`failed to get password for "%s": %s`, signet.ID, err) } continue } + // keys are _always_ signets + if signet.Scheme == SignetSchemeKey { + recipients = false + } // signet is referrer if len(signet.ID) == 0 { diff --git a/helper.go b/helper.go index 8356554..63ec8dd 100644 --- a/helper.go +++ b/helper.go @@ -70,7 +70,7 @@ func (h *Helper) RandomBytes(n int) ([]byte, error) { return RandomBytes(n) } -// Burn gets rid of the given []byte slice(s). +// Burn gets rid of the given []byte slice(s). This is currently ineffective, see known issues in the project's README. func (h *Helper) Burn(data ...[]byte) { Burn(data...) } @@ -99,7 +99,7 @@ func (h *Helper) MaxSecurityLevel() int { return defaultSecurityLevel } -// Burn gets rid of the given []byte slice(s). +// Burn gets rid of the given []byte slice(s). This is currently ineffective, see known issues in the project's README. func Burn(data ...[]byte) { for _, slice := range data { for i := 0; i < len(slice); i++ { diff --git a/letter-wire.go b/letter-wire.go index 9540814..754cf93 100644 --- a/letter-wire.go +++ b/letter-wire.go @@ -15,9 +15,7 @@ import ( - 2: Sending Keys - 4: Apply Keys - Version: varint (if Setup Msg) -- Tools: (if Setup Msg) - - Amount: varint - - Names: byte blocks +- SuiteID: byte block (if Setup Msg) - Keys: - Amount: varint - IDs/Values: byte blocks @@ -58,13 +56,8 @@ func (letter *Letter) ToWire() (*container.Container, error) { // Version: varint (if Setup Msg) c.AppendNumber(uint64(letter.Version)) - // Tools: (if Setup Msg) - // - Amount: varint - // - Names: byte blocks - c.AppendInt(len(letter.Tools)) - for _, toolName := range letter.Tools { - c.AppendAsBlock([]byte(toolName)) - } + // SuiteID: byte block (if Setup Msg) + c.AppendAsBlock([]byte(letter.SuiteID)) } if len(letter.Keys) > 0 { @@ -136,21 +129,12 @@ func LetterFromWire(c *container.Container) (*Letter, error) { } letter.Version = n - // Tools: (if Setup Msg) - // - Amount: varint - // - Names: byte blocks - n, err = c.GetNextN8() + // SuiteID: byte block (if Setup Msg) + suiteID, err := c.GetNextBlock() if err != nil { return nil, err } - letter.Tools = make([]string, n) - for i := 0; i < len(letter.Tools); i++ { - toolName, err := c.GetNextBlock() - if err != nil { - return nil, err - } - letter.Tools[i] = string(toolName) - } + letter.SuiteID = string(suiteID) } if sendingKeys { diff --git a/letter.go b/letter.go index 11c29fd..d4a2e1a 100644 --- a/letter.go +++ b/letter.go @@ -17,8 +17,8 @@ import ( // Letter is the data format for encrypted data at rest or in transit. type Letter struct { //nolint:maligned // TODO - Version uint8 // signed, MAC'd (may not exist when wired) - Tools []string // signed, MAC'd (may not exist when wired) + Version uint8 // signed, MAC'd (may not exist when wired) + SuiteID string // signed, MAC'd (may not exist when wired) Nonce []byte // signed, MAC'd Keys []*Seal `json:",omitempty"` // signed, MAC'd @@ -45,18 +45,34 @@ type Seal struct { } // Envelope returns an envelope built from the letter, configured for opening it. -func (letter *Letter) Envelope() (*Envelope, error) { +func (letter *Letter) Envelope(requirements *Requirements) (*Envelope, error) { + // basic checks if letter.Version == 0 { return nil, fmt.Errorf("letter does not specify version") } - if len(letter.Tools) == 0 { - return nil, fmt.Errorf("letter does not specify any tools") + if len(letter.SuiteID) == 0 { + return nil, fmt.Errorf("letter does not specify a suite") } + // create envelope e := &Envelope{ - Version: letter.Version, - Tools: letter.Tools, - requirements: newEmptyRequirements(), + Version: letter.Version, + SuiteID: letter.SuiteID, + } + + // get and check suite + err := e.LoadSuite() + if err != nil { + return nil, err + } + // default to full requirements + if requirements == nil { + requirements = NewRequirements() + } + // check suite against requirements + err = e.suite.Provides.CheckComplianceTo(requirements) + if err != nil { + return nil, err } for _, seal := range letter.Keys { @@ -91,13 +107,10 @@ func (letter *Letter) Envelope() (*Envelope, error) { // Open creates a session and opens the letter in one step. func (letter *Letter) Open(requirements *Requirements, trustStore TrustStore) ([]byte, error) { - e, err := letter.Envelope() + e, err := letter.Envelope(requirements) if err != nil { return nil, err } - if requirements != nil { - e.requirements = requirements - } s, err := e.Correspondence(trustStore) if err != nil { @@ -109,13 +122,10 @@ func (letter *Letter) Open(requirements *Requirements, trustStore TrustStore) ([ // Verify creates a session and verifies the letter in one step. func (letter *Letter) Verify(requirements *Requirements, trustStore TrustStore) error { - e, err := letter.Envelope() + e, err := letter.Envelope(requirements) if err != nil { return err } - if requirements != nil { - e.requirements = requirements - } s, err := e.initCorrespondence(trustStore, true) if err != nil { @@ -127,7 +137,7 @@ func (letter *Letter) Verify(requirements *Requirements, trustStore TrustStore) // WireCorrespondence creates a wire session (communication over a network connection) from a letter. func (letter *Letter) WireCorrespondence(trustStore TrustStore) (*Session, error) { - e, err := letter.Envelope() + e, err := letter.Envelope(NewRequirements().Remove(SenderAuthentication)) if err != nil { return nil, err } @@ -179,7 +189,7 @@ const ( // These IDs MUST NOT CHANGE fieldIDLetterVersion uint64 = 1 // signed, MAC'd (may not exist when wired) - fieldIDLetterTools uint64 = 2 // signed, MAC'd (may not exist when wired) + fieldIDLetterSuiteID uint64 = 2 // signed, MAC'd (may not exist when wired) fieldIDLetterNonce uint64 = 3 // signed, MAC'd fieldIDLetterKeys uint64 = 4 // signed, MAC'd fieldIDLetterMac uint64 = 5 // signed @@ -199,12 +209,9 @@ func (letter *Letter) compileAssociatedData() []byte { c.AppendNumber(fieldIDLetterVersion) // append field ID c.AppendNumber(uint64(letter.Version)) } - if len(letter.Tools) > 0 { - c.AppendNumber(fieldIDLetterTools) // append field ID - c.AppendInt(len(letter.Tools)) // append number of tools - for _, toolID := range letter.Tools { - c.AppendAsBlock([]byte(toolID)) // append field content with length - } + if len(letter.SuiteID) > 0 { + c.AppendNumber(fieldIDLetterSuiteID) // append field ID + c.AppendAsBlock([]byte(letter.SuiteID)) // append field content with length } if len(letter.Nonce) > 0 { c.AppendNumber(fieldIDLetterNonce) // append field ID diff --git a/letter_test.go b/letter_test.go index 6d051c1..1887489 100644 --- a/letter_test.go +++ b/letter_test.go @@ -11,7 +11,7 @@ import ( func TestSerialization(t *testing.T) { subject := &Letter{ Version: 1, - Tools: RecommendedNetwork, + SuiteID: SuiteComplete, Keys: []*Seal{ {ID: "a"}, {ID: "b"}, @@ -25,7 +25,7 @@ func TestSerialization(t *testing.T) { testSerialize(t, subject, true) subject.Version = 0 - subject.Tools = nil + subject.SuiteID = "" testSerialize(t, subject, true) subject.ApplyKeys = false diff --git a/password_test.go b/password_test.go index a418952..526ad81 100644 --- a/password_test.go +++ b/password_test.go @@ -2,6 +2,24 @@ package jess import "testing" +func init() { + SetPasswordCallbacks( + func(signet *Signet, minSecurityLevel int) error { + return getTestPassword(signet) + }, + getTestPassword, + ) +} + +func getTestPassword(signet *Signet) error { + pwSignet, err := testTrustStore.GetSignet(signet.ID, false) + if err != nil { + return err + } + signet.Key = pwSignet.Key + return nil +} + func TestCalculatePasswordSecurityLevel(t *testing.T) { // basic weak testPWSL(t, "asdf", -1) @@ -13,62 +31,62 @@ func TestCalculatePasswordSecurityLevel(t *testing.T) { testPWSL(t, "aaaaaaaaAAAAAAAA00000000********", -1) // chars only - testPWSL(t, "AVWHBwmF", 58) - testPWSL(t, "AVWHBwmFGt", 70) - testPWSL(t, "AVWHBwmFGtLM", 81) - testPWSL(t, "AVWHBwmFGtLMGh", 93) - testPWSL(t, "AVWHBwmFGtLMGhYf", 104) - testPWSL(t, "AVWHBwmFGtLMGhYfPkcyawfmZXRTQdxs", 195) + testPWSL(t, "AVWHBwmF", 64) + testPWSL(t, "AVWHBwmFGt", 76) + testPWSL(t, "AVWHBwmFGtLM", 87) + testPWSL(t, "AVWHBwmFGtLMGh", 98) + testPWSL(t, "AVWHBwmFGtLMGhYf", 110) + testPWSL(t, "AVWHBwmFGtLMGhYfPkcyawfmZXRTQdxs", 201) // with number - testPWSL(t, "AVWHBwm1", 60) - testPWSL(t, "AVWHBwmFG1", 72) - testPWSL(t, "AVWHBwmFGtL1", 84) - testPWSL(t, "AVWHBwmFGtLMG1", 96) - testPWSL(t, "AVWHBwmFGtLMGhY1", 108) - testPWSL(t, "AVWHBwmFGtLMGhYfPkcyawfmZXRTQdx1", 203) + testPWSL(t, "AVWHBwm1", 66) + testPWSL(t, "AVWHBwmFG1", 78) + testPWSL(t, "AVWHBwmFGtL1", 90) + testPWSL(t, "AVWHBwmFGtLMG1", 102) + testPWSL(t, "AVWHBwmFGtLMGhY1", 114) + testPWSL(t, "AVWHBwmFGtLMGhYfPkcyawfmZXRTQdx1", 209) // with number and special - testPWSL(t, "AVWHBw1_", 61) - testPWSL(t, "AVWHBwmF1_", 73) - testPWSL(t, "AVWHBwmFGt1_", 86) - testPWSL(t, "AVWHBwmFGtLM1_", 98) - testPWSL(t, "AVWHBwmFGtLMGh1_", 110) - testPWSL(t, "AVWHBwmFGtLMGhYfPkcyawfmZXRTQd1_", 207) + testPWSL(t, "AVWHBw1_", 67) + testPWSL(t, "AVWHBwmF1_", 79) + testPWSL(t, "AVWHBwmFGt1_", 91) + testPWSL(t, "AVWHBwmFGtLM1_", 103) + testPWSL(t, "AVWHBwmFGtLMGh1_", 116) + testPWSL(t, "AVWHBwmFGtLMGhYfPkcyawfmZXRTQd1_", 213) // with number and more special - testPWSL(t, "AVWHBw1*", 65) - testPWSL(t, "AVWHBwmF1*", 78) - testPWSL(t, "AVWHBwmFGt1*", 91) - testPWSL(t, "AVWHBwmFGtLM1*", 104) - testPWSL(t, "AVWHBwmFGtLMGh1*", 117) - testPWSL(t, "AVWHBwmFGtLMGhYfPkcyawfmZXRTQd1*", 221) + testPWSL(t, "AVWHBw1*", 70) + testPWSL(t, "AVWHBwmF1*", 83) + testPWSL(t, "AVWHBwmFGt1*", 96) + testPWSL(t, "AVWHBwmFGtLM1*", 109) + testPWSL(t, "AVWHBwmFGtLMGh1*", 122) + testPWSL(t, "AVWHBwmFGtLMGhYfPkcyawfmZXRTQd1*", 226) // created, strong // "Schneier scheme" // source: https://www.schneier.com/blog/archives/2014/03/choosing_secure_1.html - testPWSL(t, "WIw7,mstmsritt...", 116) - testPWSL(t, "Wow...doestcst", 94) - testPWSL(t, "Ltime@go-inag~faaa!", 135) - testPWSL(t, "uTVM,TPw55:utvm,tpwstillsecure", 210) + testPWSL(t, "WIw7,mstmsritt...", 122) + testPWSL(t, "Wow...doestcst", 100) + testPWSL(t, "Ltime@go-inag~faaa!", 140) + testPWSL(t, "uTVM,TPw55:utvm,tpwstillsecure", 216) // generated, strong - testPWSL(t, "YebGPQuuoxQwyeJMvEWACTLexUUxVBFdHYqqUybBUNfBttCvWQxDdDCdYfgMPCQp", 378) - testPWSL(t, "dpPyXmXpbECn6LWuQDJaitTTJguGfRTqNUxWfoHnBKDHvRhjR2WiQ7iDcuRJNnEd", 394) - testPWSL(t, "WgEKCp8c8{bPrG{Zo(Ms97pKt3EsR9ycz4R=kMjPp^Uafqxsd2ZTFtkfvnoueKJz", 428) - testPWSL(t, "galena-fighter-festival", 127) - testPWSL(t, "impotent-drug-dropout-damage", 152) - testPWSL(t, "artless-newswire-rill-belgium-marplot", 196) - testPWSL(t, "forbade-momenta-spook-sure-devilish-wobbly", 221) + testPWSL(t, "YebGPQuuoxQwyeJMvEWACTLexUUxVBFdHYqqUybBUNfBttCvWQxDdDCdYfgMPCQp", 383) + testPWSL(t, "dpPyXmXpbECn6LWuQDJaitTTJguGfRTqNUxWfoHnBKDHvRhjR2WiQ7iDcuRJNnEd", 400) + testPWSL(t, "WgEKCp8c8{bPrG{Zo(Ms97pKt3EsR9ycz4R=kMjPp^Uafqxsd2ZTFtkfvnoueKJz", 434) + testPWSL(t, "galena-fighter-festival", 132) + testPWSL(t, "impotent-drug-dropout-damage", 157) + testPWSL(t, "artless-newswire-rill-belgium-marplot", 202) + testPWSL(t, "forbade-momenta-spook-sure-devilish-wobbly", 227) } func testPWSL(t *testing.T, password string, expectedSecurityLevel int) { - securityLevel := CalculatePasswordSecurityLevel(password, 20000) + securityLevel := CalculatePasswordSecurityLevel(password, 1<<20) if securityLevel < expectedSecurityLevel { - t.Errorf("password %s (%di): %d - expected at least %d", password, 20000, securityLevel, expectedSecurityLevel) + t.Errorf("password %s (%di): %d - expected at least %d", password, 1<<20, securityLevel, expectedSecurityLevel) } else { - t.Logf("password %s (%di): %d", password, 20000, securityLevel) + t.Logf("password %s (%di): %d", password, 1<<20, securityLevel) } } diff --git a/requirements.go b/requirements.go index b102954..4533d7f 100644 --- a/requirements.go +++ b/requirements.go @@ -81,7 +81,7 @@ func (requirements *Requirements) CheckComplianceTo(requirement *Requirements) e } } if missing != nil { - return fmt.Errorf("missing tools with security requirements: %s", missing.String()) + return fmt.Errorf("missing security requirements: %s", missing.String()) } return nil } diff --git a/session.go b/session.go index 7a358a4..3a0a44a 100644 --- a/session.go +++ b/session.go @@ -57,15 +57,34 @@ func (sh *managedHasher) Sum() ([]byte, error) { } func newSession(e *Envelope) (*Session, error) { //nolint:gocognit,gocyclo - if len(e.Tools) == 0 { - return nil, errors.New("envelope is missing tools") + if e.suite == nil { + return nil, errors.New("suite not loaded") } + // create session s := &Session{ envelope: e, toolRequirements: newEmptyRequirements(), } + // check envelope security level + if e.SecurityLevel > 0 { + err := s.checkSecurityLevel(e.SecurityLevel, func() string { + return fmt.Sprintf(`envelope "%s"`, e.Name) + }) + if err != nil { + return nil, err + } + } + // check suite security level + err := s.checkSecurityLevel(e.suite.SecurityLevel, func() string { + return fmt.Sprintf(`suite "%s"`, e.suite.ID) + }) + if err != nil { + return nil, err + } + + // prepare variables var ( keySourceAvailable bool = false totalSignetsSeen int @@ -74,13 +93,13 @@ func newSession(e *Envelope) (*Session, error) { //nolint:gocognit,gocyclo ) // tool init loop: start - for i, toolID := range s.envelope.Tools { + for i, toolID := range s.envelope.suite.Tools { /////////////////////////////////////// // tool init loop: check for duplicates /////////////////////////////////////// - for j, dupeToolID := range s.envelope.Tools { + for j, dupeToolID := range s.envelope.suite.Tools { if i != j && toolID == dupeToolID { return nil, fmt.Errorf("cannot use tool %s twice, each tool may be only specified once", toolID) } @@ -311,11 +330,13 @@ func newSession(e *Envelope) (*Session, error) { //nolint:gocognit,gocyclo } // key signets - err := e.LoopSecrets(SignetSchemeKey, func(signet *Signet) error { + err = e.LoopSecrets(SignetSchemeKey, func(signet *Signet) error { + s.toolRequirements.Add(SenderAuthentication) + s.toolRequirements.Add(RecipientAuthentication) + totalSignetsSeen++ keySourceAvailable = true - // FIXME - return nil + return s.calcAndCheckSecurityLevel(nil, signet) }) if err != nil { return nil, err @@ -351,7 +372,7 @@ func newSession(e *Envelope) (*Session, error) { //nolint:gocognit,gocyclo if s.toolRequirements.Empty() { return nil, errors.New("envelope excludes all security requirements, no meaningful operation possible") } - err = s.toolRequirements.CheckComplianceTo(s.envelope.requirements) + err = s.toolRequirements.CheckComplianceTo(s.envelope.suite.Provides) if err != nil { return nil, err } @@ -369,7 +390,7 @@ func newSession(e *Envelope) (*Session, error) { //nolint:gocognit,gocyclo } // check if we are missing a kdf, but need one - if s.kdf == nil && len(s.signers) != len(s.envelope.Tools) { + if s.kdf == nil && len(s.signers) != len(s.envelope.suite.Tools) { return nil, errors.New("missing a key derivation tool") } @@ -394,6 +415,15 @@ func newSession(e *Envelope) (*Session, error) { //nolint:gocognit,gocyclo return nil, fmt.Errorf("detected signet or recipient in envelope that is not used by any tool") } + // check session security level + // while this should never result in an error (because every part was already checked separately) this is used as a precaution to catch errors in future code changes + err = s.checkSecurityLevel(s.SecurityLevel, func() string { + return "current session" + }) + if err != nil { + return nil, err + } + return s, nil } @@ -416,6 +446,9 @@ func (s *Session) calcAndCheckSecurityLevel(logic tools.ToolLogic, signet *Signe // existence check is done when opening/closing if len(signet.Key) > 0 { switch logic.Info().Name { + case "SCRYPT-20": + // TODO: integrate this into the tool interface + calculatedSecurityLevel = CalculatePasswordSecurityLevel(string(signet.Key), 1<<20) case "PBKDF2-SHA2-256": // TODO: integrate this into the tool interface calculatedSecurityLevel = CalculatePasswordSecurityLevel(string(signet.Key), 20000) @@ -452,38 +485,18 @@ func (s *Session) calcAndCheckSecurityLevel(logic tools.ToolLogic, signet *Signe } if signet != nil { - // signet based security level checks - switch { - case minimumSecurityLevel > 0: - // check against minimumSecurityLevel - // minimumSecurityLevel overrides other checks - if calculatedSecurityLevel < minimumSecurityLevel { - return fmt.Errorf(`supplied %s signet "%s" with a security level of %d is weaker than the desired security level of %d`, signet.Scheme, signet.ID, calculatedSecurityLevel, minimumSecurityLevel) - } - case s.envelope.MinimumSecurityLevel > 0 && calculatedSecurityLevel < s.envelope.MinimumSecurityLevel: - // check against envelope's minimum security level - return fmt.Errorf(`supplied %s signet "%s" with a security level of %d is weaker than the envelope's minimum security level of %d`, signet.Scheme, signet.ID, calculatedSecurityLevel, s.envelope.MinimumSecurityLevel) - case s.SecurityLevel > 0 && calculatedSecurityLevel < s.SecurityLevel: - // check against toolset's security level - return fmt.Errorf(`supplied %s signet "%s" with a security level of %d is weaker than the toolset's security level of %d`, signet.Scheme, signet.ID, calculatedSecurityLevel, s.SecurityLevel) - } - + err = s.checkSecurityLevel(calculatedSecurityLevel, func() string { + return fmt.Sprintf(`supplied %s signet "%s"`, signet.Scheme, signet.ID) + }) } else { - - // tool based security level checks - switch { - case minimumSecurityLevel > 0: - // check against minimumSecurityLevel - // minimumSecurityLevel overrides other checks - if calculatedSecurityLevel < minimumSecurityLevel { - return fmt.Errorf(`tool %s with a security level of %d is weaker than the desired security level of %d`, logic.Info().Name, calculatedSecurityLevel, minimumSecurityLevel) - } - case s.envelope.MinimumSecurityLevel > 0 && calculatedSecurityLevel < s.envelope.MinimumSecurityLevel: - // check against envelope's minimum security level - return fmt.Errorf(`tool %s with a security level of %d is weaker than the envelope's minimum security level of %d`, logic.Info().Name, calculatedSecurityLevel, s.envelope.MinimumSecurityLevel) - } - + // tool based securty level checks + err = s.checkSecurityLevel(calculatedSecurityLevel, func() string { + return "tool %s" + logic.Info().Name + }) + } + if err != nil { + return err } // adapt security level of session @@ -500,6 +513,42 @@ func (s *Session) calcAndCheckSecurityLevel(logic tools.ToolLogic, signet *Signe return nil } +func (s *Session) checkSecurityLevel(levelToCheck int, subject func() string) error { + switch { + case minimumSecurityLevel > 0: + // check against minimumSecurityLevel + // minimumSecurityLevel overrides other checks + if levelToCheck < minimumSecurityLevel { + return fmt.Errorf( + `%s with a security level of %d is weaker than the desired security level of %d`, + subject(), + levelToCheck, + minimumSecurityLevel, + ) + } + case s.envelope.SecurityLevel > 0: + // check against envelope's minimum security level + if levelToCheck < s.envelope.SecurityLevel { + return fmt.Errorf( + `%s with a security level of %d is weaker than the envelope's minimum security level of %d`, + subject(), + levelToCheck, + s.envelope.SecurityLevel, + ) + } + case levelToCheck < defaultSecurityLevel: + // check against default security level as fallback + return fmt.Errorf( + `%s with a security level of %d is weaker than the default minimum security level of %d`, + subject(), + levelToCheck, + defaultSecurityLevel, + ) + } + + return nil +} + // NonceSize returns the nonce size to use for new letters. func (s *Session) NonceSize() int { size := s.maxSecurityLevel / 32 diff --git a/signet.go b/signet.go index 423599a..5c3e7d7 100644 --- a/signet.go +++ b/signet.go @@ -220,11 +220,11 @@ func (signet *Signet) StoreKey() error { // Verify verifies the signature of the signet. func (signet *Signet) Verify() error { - // FIXME - return errors.New("NIY") + // TODO + return errors.New("signet verification not yet implemented") } -// Burn destroys all the key material and renders the Signet unusable. +// Burn destroys all the key material and renders the Signet unusable. This is currently ineffective, see known issues in the project's README. func (signet *Signet) Burn() error { // load tool err := signet.loadTool() diff --git a/suite.go b/suite.go new file mode 100644 index 0000000..232e95a --- /dev/null +++ b/suite.go @@ -0,0 +1,17 @@ +package jess + +// Suite status options +const ( + SuiteStatusDeprecated uint8 = 0 + SuiteStatusPermitted uint8 = 1 + SuiteStatusRecommended uint8 = 2 +) + +// Suite describes a cipher suite - a set of algorithms and the attributes they provide. +type Suite struct { + ID string + Tools []string + Provides *Requirements + SecurityLevel int + Status uint8 +} diff --git a/suites.go b/suites.go new file mode 100644 index 0000000..75b04d9 --- /dev/null +++ b/suites.go @@ -0,0 +1,100 @@ +package jess + +var ( + // lists + suitesMap = make(map[string]*Suite) + suitesList []*Suite + + // suite definitions + + // SuiteKeyV1 is a cipher suite for encryption with a key. + SuiteKeyV1 = registerSuite(&Suite{ + ID: "key_v1", + Tools: []string{"HKDF(BLAKE2b-256)", "CHACHA20-POLY1305"}, + Provides: NewRequirements(), + SecurityLevel: 128, + Status: SuiteStatusRecommended, + }) + // SuitePasswordV1 is a cipher suite for encryption with a password. + SuitePasswordV1 = registerSuite(&Suite{ + ID: "pw_v1", + Tools: []string{"SCRYPT-20", "HKDF(BLAKE2b-256)", "CHACHA20-POLY1305"}, + Provides: NewRequirements(), + SecurityLevel: 128, + Status: SuiteStatusRecommended, + }) + // SuiteRcptOnlyV1 is a cipher suite for encrypting for someone, but without verifying the sender/source. + SuiteRcptOnlyV1 = registerSuite(&Suite{ + ID: "rcpt_v1", + Tools: []string{"ECDH-X25519", "HKDF(BLAKE2b-256)", "CHACHA20-POLY1305"}, + Provides: NewRequirements().Remove(SenderAuthentication), + SecurityLevel: 128, + Status: SuiteStatusRecommended, + }) + // SuiteSignV1 is a cipher suite for signing (no encryption). + SuiteSignV1 = registerSuite(&Suite{ + ID: "sign_v1", + Tools: []string{"Ed25519(BLAKE2b-256)"}, + Provides: newEmptyRequirements().Add(SenderAuthentication), + SecurityLevel: 128, + Status: SuiteStatusRecommended, + }) + // SuiteCompleteV1 is a cipher suite for both encrypting for someone and signing. + SuiteCompleteV1 = registerSuite(&Suite{ + ID: "v1", + Tools: []string{"ECDH-X25519", "Ed25519(BLAKE2b-256)", "HKDF(BLAKE2b-256)", "CHACHA20-POLY1305"}, + Provides: NewRequirements(), + SecurityLevel: 128, + Status: SuiteStatusRecommended, + }) + // SuiteWireV1 is a cipher suite for network communication, including authentication of the server, but not the client. + SuiteWireV1 = registerSuite(&Suite{ + ID: "w1", + Tools: []string{"ECDH-X25519", "HKDF(BLAKE2b-256)", "CHACHA20-POLY1305"}, + Provides: NewRequirements().Remove(SenderAuthentication), + SecurityLevel: 128, + Status: SuiteStatusRecommended, + }) + + // currently recommended suites + + // SuiteKey is a a cipher suite for encryption with a key. + SuiteKey = SuiteKeyV1 + // SuitePassword is a a cipher suite for encryption with a password. + SuitePassword = SuitePasswordV1 + // SuiteRcptOnly is a a cipher suite for encrypting for someone, but without verifying the sender/source. + SuiteRcptOnly = SuiteRcptOnlyV1 + // SuiteSign is a a cipher suite for signing (no encryption). + SuiteSign = SuiteSignV1 + // 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. + SuiteWire = SuiteWireV1 +) + +func registerSuite(suite *Suite) (suiteID string) { + // add if not exists + _, ok := suitesMap[suite.ID] + if !ok { + suitesMap[suite.ID] = suite + suitesList = append(suitesList, suite) + } + + return suite.ID +} + +// GetSuite returns the suite with the given ID. +func GetSuite(suiteID string) (suite *Suite, ok bool) { + suite, ok = suitesMap[suiteID] + return +} + +// Suites returns all registered suites as a slice. +func Suites() []*Suite { + return suitesList +} + +// SuitesMap returns all registered suites as a map. +func SuitesMap() map[string]*Suite { + return suitesMap +} diff --git a/suites_test.go b/suites_test.go new file mode 100644 index 0000000..86a37c8 --- /dev/null +++ b/suites_test.go @@ -0,0 +1,474 @@ +package jess + +import ( + "errors" + "fmt" + "strings" + "testing" + + "github.com/safing/jess/hashtools" + "github.com/safing/jess/tools" +) + +func getSuite(t *testing.T, suiteID string) (suite *Suite) { + suite, ok := GetSuite(suiteID) + if !ok { + t.Fatalf("suite %s does not exist", suiteID) + return nil + } + return suite +} + +func TestSuites(t *testing.T) { + for _, suite := range Suites() { + + err := suiteBullshitCheck(suite) + if err != nil { + t.Errorf("suite %s has incorrect property: %s", suite.ID, err) + continue + } + + envelope, err := setupEnvelopeAndTrustStore(t, suite) + if err != nil { + t.Errorf("failed to setup test envelope for suite %s: %s", suite.ID, err) + continue + } + if envelope == nil { + t.Errorf("suite %s has an invalid toolset", suite.ID) + continue + } + + session, err := envelope.Correspondence(testTrustStore) + if err != nil { + t.Errorf("failed to init session for suite %s: %s", suite.ID, err) + continue + } + + letter, err := session.Close([]byte(testData1)) + if err != nil { + tErrorf(t, "suite %s failed to close (1): %s", suite.ID, err) + continue + } + + msg, err := letter.ToJSON() + if err != nil { + tErrorf(t, "suite %s failed to json encode (1): %s", suite.ID, err) + continue + } + + // test 2: open + + letter2, err := LetterFromJSON(msg) + if err != nil { + tErrorf(t, "suite %s failed to json decode (2): %s", suite.ID, err) + continue + } + + origData2, err := letter2.Open(envelope.suite.Provides, testTrustStore) + if err != nil { + tErrorf(t, "suite %s failed to open (2): %s", suite.ID, err) + continue + } + if string(origData2) != testData1 { + tErrorf(t, "%v original data mismatch (2): %s", suite.ID, string(origData2)) + continue + } + + // test 2.1: verify + + letter21, err := LetterFromJSON(msg) + if err != nil { + tErrorf(t, "suite %s failed to json decode (2): %s", suite.ID, err) + continue + } + + if len(letter21.Signatures) > 0 { + err = letter21.Verify(envelope.suite.Provides, testTrustStore) + if err != nil { + tErrorf(t, "suite %s failed to verify (2): %s", suite.ID, err) + continue + } + } + + } + +} + +func suiteBullshitCheck(suite *Suite) error { //nolint:gocognit,gocyclo + // pre checks + if suite.Provides == nil { + return errors.New("provides no requirement attributes") + } + if suite.SecurityLevel == 0 { + return errors.New("does not specify security level") + } + + // create session struct for holding information + s := &Session{ + envelope: &Envelope{ + suite: suite, + }, + toolRequirements: newEmptyRequirements(), + } + + // check if we are assuming we have a key + assumeKey := strings.Contains(suite.ID, "key") + if assumeKey { + s.toolRequirements.Add(SenderAuthentication) + s.toolRequirements.Add(RecipientAuthentication) + } + + // tool check loop: start + for i, toolID := range suite.Tools { + + //////////////////////////////////////// + // tool check loop: check for duplicates + //////////////////////////////////////// + + for j, dupeToolID := range suite.Tools { + if i != j && toolID == dupeToolID { + return fmt.Errorf("cannot use tool %s twice, each tool may be only specified once", toolID) + } + } + + /////////////////////////////////////// + // tool check loop: parse, prep and get + /////////////////////////////////////// + + var ( + hashTool *hashtools.HashTool + hashSumFn func() ([]byte, error) + ) + + // parse ID for args + var arg string + if strings.Contains(toolID, "(") { + splitted := strings.Split(toolID, "(") + toolID = splitted[0] + arg = strings.Trim(splitted[1], "()") + } + + // get tool + tool, err := tools.Get(toolID) + if err != nil { + return fmt.Errorf("the specified tool %s could not be found", toolID) + } + + // create logic instance and add to logic and state lists + logic := tool.Factory() + s.all = append(s.all, logic) + if tool.Info.HasOption(tools.OptionHasState) { + s.toolsWithState = append(s.toolsWithState, logic) + } + + /////////////////////////////////////////////////////////////// + // tool check loop: assign tools to queues and add requirements + /////////////////////////////////////////////////////////////// + + switch tool.Info.Purpose { + case tools.PurposeKeyDerivation: + if s.kdf != nil { + return fmt.Errorf("cannot use %s, you may only specify one key derivation tool and %s was already specified", tool.Info.Name, s.kdf.Info().Name) + } + s.kdf = logic + + case tools.PurposePassDerivation: + if s.passDerivator != nil { + return fmt.Errorf("cannot use %s, you may only specify one password derivation tool and %s was already specified", tool.Info.Name, s.passDerivator.Info().Name) + } + s.passDerivator = logic + s.toolRequirements.Add(SenderAuthentication) + s.toolRequirements.Add(RecipientAuthentication) + + case tools.PurposeKeyExchange: + s.keyExchangers = append(s.keyExchangers, logic) + s.toolRequirements.Add(RecipientAuthentication) + + case tools.PurposeKeyEncapsulation: + s.keyEncapsulators = append(s.keyEncapsulators, logic) + s.toolRequirements.Add(RecipientAuthentication) + + case tools.PurposeSigning: + s.signers = append(s.signers, logic) + s.toolRequirements.Add(SenderAuthentication) + + case tools.PurposeIntegratedCipher: + s.integratedCiphers = append(s.integratedCiphers, logic) + s.toolRequirements.Add(Confidentiality) + s.toolRequirements.Add(Integrity) + + case tools.PurposeCipher: + s.ciphers = append(s.ciphers, logic) + s.toolRequirements.Add(Confidentiality) + + case tools.PurposeMAC: + s.macs = append(s.macs, logic) + s.toolRequirements.Add(Integrity) + } + + //////////////////////////////////////////////// + // tool check loop: process options, get hashers + //////////////////////////////////////////////// + + for _, option := range tool.Info.Options { + switch option { + + case tools.OptionNeedsManagedHasher: + // get managed hasher list + var managedHashers map[string]*managedHasher + switch tool.Info.Purpose { + case tools.PurposeMAC: + if s.managedMACHashers == nil { + s.managedMACHashers = make(map[string]*managedHasher) + } + managedHashers = s.managedMACHashers + case tools.PurposeSigning: + if s.managedSigningHashers == nil { + s.managedSigningHashers = make(map[string]*managedHasher) + } + managedHashers = s.managedSigningHashers + default: + return fmt.Errorf("only MAC and Signing tools may use managed hashers") + } + + // get or assign a new managed hasher + mngdHasher, ok := managedHashers[arg] + if !ok { + // get hashtool + ht, err := hashtools.Get(arg) + if err != nil { + return fmt.Errorf("the specified hashtool for %s(%s) could not be found", toolID, arg) + } + + // save to managed hashers + mngdHasher = &managedHasher{ + tool: ht, + hash: ht.New(), + } + managedHashers[arg] = mngdHasher + } + + hashTool = mngdHasher.tool + hashSumFn = mngdHasher.Sum + + case tools.OptionNeedsDedicatedHasher: + hashTool, err = hashtools.Get(arg) + if err != nil { + return fmt.Errorf("the specified hashtool for %s(%s) could not be found", toolID, arg) + } + + } + } + + /////////////////////////////////// + // tool check loop: initialize tool + /////////////////////////////////// + + // init tool + logic.Init( + tool, + &Helper{ + session: s, + info: tool.Info, + }, + hashTool, + hashSumFn, + ) + + ////////////////////////////////////////////////// + // tool check loop: calc and check security levels + ////////////////////////////////////////////////// + + err = s.calcAndCheckSecurityLevel(logic, nil) + if err != nil { + return err + } + + } // tool check loop: end + + /////////////// + // final checks + /////////////// + + // check requirements requirements + if s.toolRequirements.Empty() { + return errors.New("suite does not provide any security attributes") + } + + // check if we have recipient auth without confidentiality + if s.toolRequirements.Has(RecipientAuthentication) && + !s.toolRequirements.Has(Confidentiality) { + return errors.New("having recipient authentication without confidentiality does not make sense") + } + + // check if we have confidentiality without integrity + if s.toolRequirements.Has(Confidentiality) && + !s.toolRequirements.Has(Integrity) { + return errors.New("having confidentiality without integrity does not make sense") + } + + // check if we are missing a kdf, but need one + if s.kdf == nil && len(s.signers) != len(s.envelope.suite.Tools) { + return errors.New("missing a key derivation tool") + } + + // check if have a kdf, even if we don't need one + if len(s.integratedCiphers) == 0 && + len(s.ciphers) == 0 && + len(s.macs) == 0 && + s.kdf != nil { + return errors.New("key derivation tool specified, but not needed") + } + + ///////////////////////////////////////// + // check if values match suite definition + ///////////////////////////////////////// + + // check if security level matches + if s.SecurityLevel != suite.SecurityLevel { + return fmt.Errorf("suite has incorrect security level: %d (expected %d)", suite.SecurityLevel, s.SecurityLevel) + } + + // check if requirements match + if s.toolRequirements.SerializeToNoSpec() != suite.Provides.SerializeToNoSpec() { + return fmt.Errorf( + "suite has incorrect attributes: no %s (expected no %s)", + suite.Provides.SerializeToNoSpec(), + s.toolRequirements.SerializeToNoSpec(), + ) + } + + /////////////////////////////////////////////////////////// + // check if computeSuiteAttributes returns the same results + /////////////////////////////////////////////////////////// + + computedSuite := computeSuiteAttributes(suite.Tools, assumeKey) + if computedSuite == nil { + return errors.New("internal error: could not compute suite attributes") + } + if suite.SecurityLevel != computedSuite.SecurityLevel { + return fmt.Errorf("internal error: computeSuiteAttributes error: security level: suite=%d computed=%d", suite.SecurityLevel, computedSuite.SecurityLevel) + } + if suite.Provides.SerializeToNoSpec() != computedSuite.Provides.SerializeToNoSpec() { + return fmt.Errorf( + "internal error: computeSuiteAttributes error: attributes: suite=no %s compute=no %s)", + suite.Provides.SerializeToNoSpec(), + computedSuite.Provides.SerializeToNoSpec(), + ) + } + + return nil +} + +func computeSuiteAttributes(toolIDs []string, assumeKey bool) *Suite { + new := &Suite{ + Provides: newEmptyRequirements(), + SecurityLevel: 0, + } + + // if we have a key + if assumeKey { + new.Provides.Add(SenderAuthentication) + new.Provides.Add(RecipientAuthentication) + } + + // check all security levels and collect attributes + for _, toolID := range toolIDs { + + /////////////////////////////////////// + // tool check loop: parse, prep and get + /////////////////////////////////////// + + var hashTool *hashtools.HashTool + + // parse ID for args + var arg string + if strings.Contains(toolID, "(") { + splitted := strings.Split(toolID, "(") + toolID = splitted[0] + arg = strings.Trim(splitted[1], "()") + } + + // get tool + tool, err := tools.Get(toolID) + if err != nil { + return nil + } + + // create logic instance and add to logic and state lists + logic := tool.Factory() + + ////////////////////////////////////// + // tool check loop: collect attributes + ////////////////////////////////////// + + switch tool.Info.Purpose { + case tools.PurposePassDerivation: + new.Provides.Add(SenderAuthentication) + new.Provides.Add(RecipientAuthentication) + + case tools.PurposeKeyExchange: + new.Provides.Add(RecipientAuthentication) + + case tools.PurposeKeyEncapsulation: + new.Provides.Add(RecipientAuthentication) + + case tools.PurposeSigning: + new.Provides.Add(SenderAuthentication) + + case tools.PurposeIntegratedCipher: + new.Provides.Add(Confidentiality) + new.Provides.Add(Integrity) + + case tools.PurposeCipher: + new.Provides.Add(Confidentiality) + + case tools.PurposeMAC: + new.Provides.Add(Integrity) + } + + //////////////////////////////////////////////// + // tool check loop: process options, get hashers + //////////////////////////////////////////////// + + for _, option := range tool.Info.Options { + switch option { + case tools.OptionNeedsManagedHasher, + tools.OptionNeedsDedicatedHasher: + hashTool, err = hashtools.Get(arg) + if err != nil { + return nil + } + } + } + + /////////////////////////////////// + // tool check loop: initialize tool + /////////////////////////////////// + + // init tool + logic.Init( + tool, + &Helper{ + info: tool.Info, + }, + hashTool, + nil, + ) + + ////////////////////////////////////////// + // tool check loop: compute security level + ////////////////////////////////////////// + + toolSecurityLevel, err := logic.SecurityLevel(nil) + if err != nil { + return nil + } + if new.SecurityLevel == 0 || toolSecurityLevel < new.SecurityLevel { + new.SecurityLevel = toolSecurityLevel + } + + } + + return new +} diff --git a/tools/ecdh/nist.go b/tools/ecdh/nist.go index accd941..84c3550 100644 --- a/tools/ecdh/nist.go +++ b/tools/ecdh/nist.go @@ -147,7 +147,7 @@ func (ec *NistCurve) GenerateKey(signet tools.SignetInt) error { return nil } -// BurnKey implements the ToolLogic interface. +// BurnKey implements the ToolLogic interface. This is currently ineffective, see known issues in the project's README. func (ec *NistCurve) BurnKey(signet tools.SignetInt) error { pubKey := signet.PublicKey() privKey := signet.PrivateKey() diff --git a/tools/ecdh/x25519.go b/tools/ecdh/x25519.go index 7b799a4..e4090cc 100644 --- a/tools/ecdh/x25519.go +++ b/tools/ecdh/x25519.go @@ -115,7 +115,7 @@ func (ec *X25519Curve) GenerateKey(signet tools.SignetInt) error { return nil } -// BurnKey implements the ToolLogic interface. +// BurnKey implements the ToolLogic interface. This is currently ineffective, see known issues in the project's README. func (ec *X25519Curve) BurnKey(signet tools.SignetInt) error { pubKey := signet.PublicKey() privKey := signet.PrivateKey() diff --git a/tools/gostdlib/ed25519.go b/tools/gostdlib/ed25519.go index 163c7f8..d7b3e08 100644 --- a/tools/gostdlib/ed25519.go +++ b/tools/gostdlib/ed25519.go @@ -148,7 +148,7 @@ func (ed *Ed25519) GenerateKey(signet tools.SignetInt) error { return nil } -// BurnKey implements the ToolLogic interface. +// BurnKey implements the ToolLogic interface. This is currently ineffective, see known issues in the project's README. func (ed *Ed25519) BurnKey(signet tools.SignetInt) error { pubKey := signet.PublicKey() privKey := signet.PrivateKey() diff --git a/tools/gostdlib/rsa-keys.go b/tools/gostdlib/rsa-keys.go index 3e87645..03fc981 100644 --- a/tools/gostdlib/rsa-keys.go +++ b/tools/gostdlib/rsa-keys.go @@ -104,7 +104,7 @@ func (base *rsaBase) GenerateKey(signet tools.SignetInt) error { return nil } -// BurnKey implements the ToolLogic interface. +// BurnKey implements the ToolLogic interface. This is currently ineffective, see known issues in the project's README. func (base *rsaBase) BurnKey(signet tools.SignetInt) error { pubKey := signet.PublicKey() privKey := signet.PrivateKey() diff --git a/tools/gostdlib/rsa-oaep.go b/tools/gostdlib/rsa-oaep.go index 976a6de..b15756c 100644 --- a/tools/gostdlib/rsa-oaep.go +++ b/tools/gostdlib/rsa-oaep.go @@ -35,6 +35,9 @@ func (oaep *RsaOAEP) EncapsulateKey(key []byte, signet tools.SignetInt) ([]byte, if !ok { return nil, tools.ErrInvalidKey } + if rsaPubKey == nil { + return nil, tools.ErrInvalidKey + } // check key length: The message must be no longer than the length of the public modulus minus twice the hash length, minus a further 2. maxMsgSize := rsaPubKey.Size() - (2 * oaep.HashTool().DigestSize) - 2 diff --git a/tools/gostdlib/scrypt.go b/tools/gostdlib/scrypt.go new file mode 100644 index 0000000..8285370 --- /dev/null +++ b/tools/gostdlib/scrypt.go @@ -0,0 +1,40 @@ +package gostdlib + +import ( + "golang.org/x/crypto/scrypt" + + "github.com/safing/jess/tools" +) + +func init() { + tools.Register(&tools.Tool{ + Info: &tools.ToolInfo{ + Name: "SCRYPT-20", + Purpose: tools.PurposePassDerivation, + Options: []uint8{tools.OptionNeedsDefaultKeySize}, + SecurityLevel: 0, // security of default key size + Comment: "", + Author: "Colin Percival, 2009", + }, + Factory: func() tools.ToolLogic { + return &SCRYPT{ + n: 1 << 20, // 2^20 resp. 1,048,576 - CPU/memory cost parameter + r: 8, // The blocksize parameter + p: 1, // Parallelization parameter + } + }, + }) +} + +// SCRYPT implements the cryptographic interface for SCRYPT password derivation. +type SCRYPT struct { + tools.ToolLogicBase + n int // CPU/memory cost parameter + r int // The blocksize parameter + p int // Parallelization parameter +} + +// DeriveKeyFromPassword implements the ToolLogic interface. +func (sc *SCRYPT) DeriveKeyFromPassword(password []byte, salt []byte) ([]byte, error) { + return scrypt.Key(password, salt, sc.n, sc.r, sc.p, sc.Helper().DefaultSymmetricKeySize()) +} diff --git a/tools/interfaces.go b/tools/interfaces.go index 01c638c..8b7b5e8 100644 --- a/tools/interfaces.go +++ b/tools/interfaces.go @@ -22,7 +22,7 @@ type HelperInt interface { // RandomBytes returns the specified amount of random bytes in a []byte slice. RandomBytes(n int) ([]byte, error) - // Burn gets rid of the given []byte slice(s). + // Burn gets rid of the given []byte slice(s). This is currently ineffective, see known issues in the project's README. Burn(data ...[]byte) // DefaultSymmetricKeySize returns the default key size for this session. diff --git a/tools/toollogic.go b/tools/toollogic.go index 198b8b9..b93a564 100644 --- a/tools/toollogic.go +++ b/tools/toollogic.go @@ -119,6 +119,7 @@ type ToolLogic interface { // BurnKey deletes the loaded keys in the Signet. // Must work with a static (no Setup()) ToolLogic. // Must be overridden by tools that declare FeatureKeyExchange, FeatureKeyEncapsulation or FeatureSigning. + // Implementations of this are currently ineffective, see known issues in the project's README. BurnKey(signet SignetInt) error // SecurityLevel returns the security level (approximate attack complexity as 2^n) of the given tool. @@ -289,7 +290,7 @@ func (tlb *ToolLogicBase) GenerateKey(signet SignetInt) error { return ErrNotImplemented } -// BurnKey implements the ToolLogic interface. +// BurnKey implements the ToolLogic interface. This is currently ineffective, see known issues in the project's README. func (tlb *ToolLogicBase) BurnKey(signet SignetInt) error { return ErrNotImplemented } diff --git a/tools_test.go b/tools_test.go index 041bbcc..b3eccac 100644 --- a/tools_test.go +++ b/tools_test.go @@ -1,7 +1,12 @@ package jess import ( + "bytes" + "encoding/json" "testing" + "time" + + "github.com/safing/jess/hashtools" "github.com/safing/jess/tools" @@ -32,7 +37,39 @@ func TestConformity(t *testing.T) { } +func TestPasswordHashingSpeed(t *testing.T) { + // skip in short tests and when not running comprehensive + if testing.Short() || !runComprehensiveTestsActive { + return + } + // run this test with + // go test -timeout 10m github.com/safing/jess -v -count=1 -ldflags "-X github.com/safing/jess.RunComprehensiveTests=true" -run ^TestPasswordHashingSpeed$ + + for _, tool := range tools.AsList() { + if tool.Info.Purpose == tools.PurposePassDerivation { + password := []byte(testPassword1) + salt, err := RandomBytes(4) + if err != nil { + t.Fatal(err) + } + + start := time.Now() + _, err = tool.StaticLogic.DeriveKeyFromPassword(password, salt) + if err != nil { + t.Fatal(err) + } + t.Logf("%s took %s to derive key from password", tool.Info.Name, time.Since(start)) + } + } +} + +//nolint:gocognit,gocyclo func TestSignetHandling(t *testing.T) { + hashTool, err := hashtools.Get("SHA2-256") + if err != nil { + t.Fatal(err) + } + for _, tool := range tools.AsList() { switch tool.Info.Purpose { case tools.PurposeKeyExchange, @@ -88,6 +125,128 @@ func TestSignetHandling(t *testing.T) { t.Fatalf("failed to load %s recipient: %s", tool.Info.Name, err) } + // store signet + signetJSON, err := json.Marshal(signet) + if err != nil { + t.Fatalf("failed to serialize %s signet: %s", tool.Info.Name, err) + } + + // load signet + loadedSignet := &Signet{} + err = json.Unmarshal(signetJSON, loadedSignet) + if err != nil { + t.Fatalf("failed to parse serialized %s signet: %s", tool.Info.Name, err) + } + err = loadedSignet.LoadKey() + if err != nil { + t.Fatalf("failed to load key of %s signet: %s", tool.Info.Name, err) + } + + // store rcpt + rcptJSON, err := json.Marshal(rcpt) + if err != nil { + t.Fatalf("failed to serialize %s rcpt: %s", tool.Info.Name, err) + } + + // load rcpt + loadedRcpt := &Signet{} + err = json.Unmarshal(rcptJSON, loadedRcpt) + if err != nil { + t.Fatalf("failed to parse serialized %s rcpt: %s", tool.Info.Name, err) + } + err = loadedRcpt.LoadKey() + if err != nil { + t.Fatalf("failed to load key of %s rcpt: %s", tool.Info.Name, err) + } + + // load tool + err = loadedSignet.loadTool() + if err != nil { + t.Fatalf("failed to load tool of %s: %s", tool.Info.Name, err) + } + + // init tool with hashtool + toolLogic := tool.Factory() + hasher := managedHasher{ + tool: hashTool, + hash: hashTool.New(), + } + toolLogic.Init( + tool, + &Helper{info: tool.Info}, + hashTool, + hasher.Sum, + ) + + // do an operation + switch loadedSignet.tool.Info.Purpose { + case tools.PurposeKeyExchange: + // create and generate signet + peerSignet := NewSignetBase(tool) + err := peerSignet.GenerateKey() + if err != nil { + t.Fatalf("failed to generate key with %s peer: %s", tool.Info.Name, err) + } + + // transform to recipient + peerRcpt, err := peerSignet.AsRecipient() + if err != nil { + t.Fatalf("failed to get %s peer as recipient: %s", tool.Info.Name, err) + } + + sharedSecret1, err := toolLogic.MakeSharedKey(loadedSignet, peerRcpt) + if err != nil { + t.Fatalf("failed to make shared secret 1 with %s: %s", tool.Info.Name, err) + } + + sharedSecret2, err := toolLogic.MakeSharedKey(peerSignet, loadedRcpt) + if err != nil { + t.Fatalf("failed to make shared secret 2 with %s: %s", tool.Info.Name, err) + } + + if !bytes.Equal(sharedSecret1, sharedSecret2) { + t.Fatalf("shared secrets made with %s do not match, got:\ns1: %v\ns2: %v", tool.Info.Name, sharedSecret1, sharedSecret2) + } + case tools.PurposeKeyEncapsulation: + origKey, err := RandomBytes(16) + if err != nil { + t.Fatalf("failed to generate test key: %s", err) + } + + wrappedKey, err := toolLogic.EncapsulateKey(origKey, loadedRcpt) + if err != nil { + t.Fatalf("failed to encapsulate key with %s: %s", tool.Info.Name, err) + } + + unwrappedKey, err := toolLogic.UnwrapKey(wrappedKey, loadedSignet) + if err != nil { + t.Fatalf("failed to unwrap key with %s: %s", tool.Info.Name, err) + } + + if !bytes.Equal(origKey, unwrappedKey) { + t.Fatalf("original and unwrapped key with %s do not match, got:\norig: %v\nunwrapped: %v", tool.Info.Name, origKey, unwrappedKey) + } + + case tools.PurposeSigning: + testData, err := RandomBytes(16) + if err != nil { + t.Fatalf("failed to generate test data: %s", err) + } + _, err = hasher.hash.Write(testData) + if err != nil { + t.Fatalf("failed to write to hash: %s", err) + } + + signature, err := toolLogic.Sign(testData, nil, loadedSignet) + if err != nil { + t.Fatalf("failed to sign with %s: %s", tool.Info.Name, err) + } + + err = toolLogic.Verify(testData, nil, signature, loadedRcpt) + if err != nil { + t.Fatalf("failed to verify with %s: %s", tool.Info.Name, err) + } + } } } diff --git a/truststores/io.go b/truststores/io.go index 282853f..ff420c3 100644 --- a/truststores/io.go +++ b/truststores/io.go @@ -65,9 +65,6 @@ func WriteEnvelopeToFile(envelope *jess.Envelope, filename string) error { return errInvalidEnvelopeNameChars } - // serialize requirements - envelope.SerializeRequirements() - // serialize data, err := dsd.DumpIndent(envelope, dsd.JSON, "\t") if err != nil { @@ -100,8 +97,8 @@ func LoadEnvelopeFromFile(filename string) (*jess.Envelope, error) { return nil, err } - // parse requirements from "No" - err = envelope.LoadRequirements() + // load suite using SuiteID + err = envelope.LoadSuite() if err != nil { return nil, err }