safing-jess/core_test.go
2023-09-26 13:26:44 +02:00

537 lines
15 KiB
Go

package jess
import (
"errors"
"fmt"
"strings"
"testing"
"time"
"github.com/safing/jess/tools"
)
const (
testData1 = "The quick brown fox jumps over the lazy dog. "
testData2 = `Tempora aut rerum ut esse illo. Aut quo qui rem consequuntur quia suscipit nihil labore. Quisquam et corrupti exercitationem nesciunt. Ut nisi voluptas natus.
Nihil ipsum maxime necessitatibus distinctio velit. Debitis perferendis suscipit ut. Aut illum blanditiis ut voluptates qui.
Consequatur cupiditate itaque qui quam. Sed qui velit et aperiam voluptatum reprehenderit aut voluptas. Eaque facere sit exercitationem dolore quos eum. Consectetur est voluptas nemo dolor rerum quae. Nisi sed velit quasi alias assumenda.
Commodi eos asperiores fugiat molestiae ea eos eligendi. Explicabo illo quisquam et ut incidunt libero vel eius. Libero accusamus corporis cum rem. Voluptas molestias corporis veritatis sapiente nihil voluptatibus. Delectus sit qui iste ut vero. Occaecati reiciendis ex sed consequuntur dolor et.
Qui voluptates quod omnis rerum. Soluta dolore quia eius quo similique accusamus. Quisquam fugiat sed voluptatibus eos earum sed. Numquam quia at commodi aut esse ducimus enim.
Enim nihil architecto architecto. Reprehenderit at assumenda labore. Et ut sed ut inventore tenetur autem. Iusto et neque ab dolores eum. Praesentium amet sint ut voluptate impedit sit.
A accusantium ullam voluptatibus. Adipisci architecto minus dolore tenetur eos. Id illum quo neque laborum numquam laborum animi libero.
Debitis voluptatem non aut ex. Et et quis qui aut fugit accusantium. Est dolor quia accusantium culpa.
Facere iste dolor a qui. Earum aut facilis maxime repudiandae magnam. Laborum illum distinctio quo libero corrupti maxime. Eum nam officiis culpa nobis.
Et repellat qui ut quaerat error explicabo. Distinctio repudiandae sit dolores nam at. Suscipit aliquam alias ullam id.`
testPassword1 = "Jt0gYfUh0mMsWH1jYhOI2SXQ8rKMmu38pkBgDa6p8YlOlae" //nolint:gosec
testPassword2 = "6+cYgtpM6CYjApRvc+ayx4t4zXJ9PSr80ykp3jmwagATaw4" //nolint:gosec
testHasher = "SHA2-256"
)
var (
testKey1 []byte
testKey2 []byte
testTrustStore = NewMemTrustStore()
RunComprehensiveTests string
runComprehensiveTestsActive bool
RunTestsInDebugStyle string
runTestsInDebugStyleActive bool
debugStyleMaxErrors = 10
debugStyleErrorCnt int
)
func tErrorf(t *testing.T, msg string, args ...interface{}) {
t.Helper()
t.Errorf(msg, args...)
if runTestsInDebugStyleActive {
debugStyleErrorCnt++
if debugStyleErrorCnt >= debugStyleMaxErrors {
t.Skipf("reached %d errors, ending early", debugStyleErrorCnt)
}
}
}
func init() {
// init test key
var err error
testKey1, err = RandomBytes(defaultSymmetricKeySize)
if err != nil {
panic(err)
}
testKey2, err = RandomBytes(defaultSymmetricKeySize)
if err != nil {
panic(err)
}
// init trust store
err = testTrustStore.StoreSignet(&Signet{
Version: 1,
ID: "test-key-1",
Scheme: SignetSchemeKey,
Key: testKey1,
})
if err != nil {
panic(err)
}
err = testTrustStore.StoreSignet(&Signet{
Version: 1,
ID: "test-key-2",
Scheme: SignetSchemeKey,
Key: testKey2,
})
if err != nil {
panic(err)
}
err = testTrustStore.StoreSignet(&Signet{
Version: 1,
ID: "test-pw-1",
Scheme: SignetSchemePassword,
Key: []byte(testPassword1),
})
if err != nil {
panic(err)
}
err = testTrustStore.StoreSignet(&Signet{
Version: 1,
ID: "test-pw-2",
Scheme: SignetSchemePassword,
Key: []byte(testPassword2),
})
if err != nil {
panic(err)
}
// lower defaults for better test speed
defaultSymmetricKeySize = 16
defaultSecurityLevel = 128
// init special test config
if RunComprehensiveTests == "true" {
runComprehensiveTestsActive = true
}
if RunTestsInDebugStyle == "true" {
runTestsInDebugStyleActive = true
}
}
func TestCoreBasic(t *testing.T) {
t.Parallel()
for _, suite := range Suites() {
testStorage(t, suite)
}
}
// TestCoreAllCombinations tests all tools in all combinations and every tool
// should be tested when placed before and after every other tool.
func TestCoreAllCombinations(t *testing.T) {
t.Parallel()
// 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
if runTestsInDebugStyleActive {
for _, tool := range tools.AsList() {
all = append(all, tool.Info.Name)
}
} else {
for _, tool := range tools.AsMap() {
all = append(all, tool.Info.Name)
}
}
// add hashers to tools that need them
for i := 0; i < len(all); i++ {
// get tool
tool, err := tools.Get(all[i])
if err != nil {
t.Fatalf("failed to get tool %s: %s", all[i], err)
return
}
// add hasher if needed
if tool.Info.HasOption(tools.OptionNeedsManagedHasher) ||
tool.Info.HasOption(tools.OptionNeedsDedicatedHasher) {
all[i] = fmt.Sprintf("%s(%s)", all[i], testHasher)
}
}
// compute all combinations
combinations := generateCombinations(all)
combinationsTested := 0
combinationsDetectedInvalid := 0
if runComprehensiveTestsActive {
fmt.Println("running comprehensive tests, printing one dot per 1000 combinations tested.")
}
for _, testTools := range combinations {
// >4 tools && !comprehensive: don't test
if !runComprehensiveTestsActive && len(testTools) > 4 {
continue
}
if len(testTools) == 4 &&
runComprehensiveTestsActive {
// ==4 tools && comprehensive: rotate
// if we want to test before/after differences, we need to use at least 4 tools, because if we have 2 key exchanges, they need at least an aead cipher and key derivation tool in order to work.
// rotate to test before/after differences
for i := 0; i < len(testTools); i++ {
detectedInvalid := testStorage(t, &Suite{Tools: testTools})
combinationsTested++
if detectedInvalid {
combinationsDetectedInvalid++
}
if runComprehensiveTestsActive && combinationsTested%1000 == 0 {
fmt.Print(".")
}
// rotate
testTools = append(testTools, testTools[0])[1:]
}
} else {
// test this order only
detectedInvalid := testStorage(t, &Suite{Tools: testTools})
combinationsTested++
if detectedInvalid {
combinationsDetectedInvalid++
}
if runComprehensiveTestsActive && combinationsTested%1000 == 0 {
fmt.Print(".")
}
}
}
if runComprehensiveTestsActive {
fmt.Println("\n\nfinished.")
}
t.Logf("tested %d tool combinations", combinationsTested)
t.Logf("of these, %d were successfully detected as invalid", combinationsDetectedInvalid)
}
func testStorage(t *testing.T, suite *Suite) (detectedInvalid bool) { //nolint:thelper
t.Logf("testing storage with %s", suite.ID)
e, err := setupEnvelopeAndTrustStore(t, suite)
if err != nil {
tErrorf(t, "%s failed: %s", suite.ID, err)
return false
}
if e == nil {
return true
}
// test 1: close
s, err := e.Correspondence(testTrustStore)
if err != nil {
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, "%s failed to close (1): %s", suite.ID, err)
return false
}
msg, err := letter.ToJSON()
if err != nil {
tErrorf(t, "%s failed to json encode (1): %s", suite.ID, err)
return false
}
// test 2: open
letter2, err := LetterFromJSON(msg)
if err != nil {
tErrorf(t, "%s failed to json decode (2): %s", suite.ID, err)
return false
}
origData2, err := letter2.Open(e.suite.Provides, testTrustStore)
if err != nil {
tErrorf(t, "%s failed to open (2): %s", suite.ID, err)
return false
}
if string(origData2) != testData1 {
tErrorf(t, "%s original data mismatch (2): %s", suite.ID, string(origData2))
return false
}
// test 2.1: verify
letter21, err := LetterFromJSON(msg)
if err != nil {
tErrorf(t, "%s failed to json decode (2): %s", suite.ID, err)
return false
}
if len(letter21.Signatures) > 0 {
err = letter21.Verify(e.suite.Provides, testTrustStore)
if err != nil {
tErrorf(t, "%s failed to verify (2): %s", suite.ID, err)
return false
}
}
return false
}
//nolint:gocognit,gocyclo
func setupEnvelopeAndTrustStore(t *testing.T, suite *Suite) (*Envelope, error) {
t.Helper()
// 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 := NewUnconfiguredEnvelope()
e.SuiteID = suite.ID
e.suite = suite
// check vars
keyDerPresent := false
passDerPresent := false
asyncKeyEstablishmentPresent := false
// process tools and setup envelope
for _, toolID := range e.suite.Tools {
// remove hasher argument for now
if strings.Contains(toolID, "(") {
toolID = strings.Split(toolID, "(")[0]
}
// get tool
tool, err := tools.Get(toolID)
if err != nil {
return nil, err
}
// generate needed signets
switch tool.Info.Purpose {
case tools.PurposePassDerivation:
pw, err := getOrMakeSignet(t, nil, false, "test-pw-1")
if err != nil {
return nil, err
}
e.Secrets = append(e.Secrets, pw)
// add a second one!
if len(suite.Tools) <= 2 {
pw1, err := getOrMakeSignet(t, nil, false, "test-pw-2")
if err != nil {
return nil, err
}
e.Secrets = append(e.Secrets, pw1)
}
case tools.PurposeKeyExchange, tools.PurposeKeyEncapsulation:
asyncKeyEstablishmentPresent = true
recipient, err := getOrMakeSignet(t, tool.StaticLogic, true, fmt.Sprintf("test-%s", tool.Info.Name))
if err != nil {
return nil, err
}
e.Recipients = append(e.Recipients, recipient)
case tools.PurposeSigning:
sender, err := getOrMakeSignet(t, tool.StaticLogic, false, fmt.Sprintf("test-%s", tool.Info.Name))
if err != nil {
return nil, err
}
e.Senders = append(e.Senders, sender)
}
// add required requirements
switch tool.Info.Purpose {
case tools.PurposeKeyDerivation:
keyDerPresent = true
case tools.PurposePassDerivation:
passDerPresent = true
// add passderivation requirements later, as it is a bit special
case tools.PurposeKeyExchange:
e.suite.Provides.Add(RecipientAuthentication)
case tools.PurposeKeyEncapsulation:
e.suite.Provides.Add(RecipientAuthentication)
case tools.PurposeSigning:
e.suite.Provides.Add(Integrity)
e.suite.Provides.Add(SenderAuthentication)
case tools.PurposeIntegratedCipher:
e.suite.Provides.Add(Confidentiality)
e.suite.Provides.Add(Integrity)
case tools.PurposeCipher:
e.suite.Provides.Add(Confidentiality)
case tools.PurposeMAC:
e.suite.Provides.Add(Integrity)
}
}
// if invalid: test if toolset is recognized as invalid
// no requirements -> only "meta" tools (kdf, pass derivation)
if e.suite.Provides.Empty() {
return nil, testInvalidToolset(e, "there are only meta tools in toolset")
}
// recipient auth, but no confidentiality? nope.
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.Senders) != len(e.suite.Tools) {
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.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.suite.Provides.Add(SenderAuthentication)
e.suite.Provides.Add(RecipientAuthentication)
// need Confidentiality for this to make sense
if !e.suite.Provides.Has(Confidentiality) {
return nil, testInvalidToolset(e, "using a password without confidentiality does not make sense")
}
}
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
}
e.Secrets = append(e.Secrets, key)
// add a second one!
if len(suite.Tools) <= 2 {
key2, err := getOrMakeSignet(t, nil, false, "test-key-2")
if err != nil {
return nil, err
}
e.Secrets = append(e.Secrets, key2)
}
}
return e, nil
}
func testInvalidToolset(e *Envelope, whyInvalid string) error {
if e.Check(testTrustStore) == nil {
return fmt.Errorf("passed check although %s", whyInvalid)
}
return nil
}
func getOrMakeSignet(t *testing.T, tool tools.ToolLogic, recipient bool, signetID string) (*Signet, error) {
t.Helper()
// check if signet already exists
signet, err := testTrustStore.GetSignet(signetID, recipient)
if err == nil {
return signet, nil
}
// handle special cases
if tool == nil {
return nil, errors.New("bad parameters")
}
// create new signet
newSignet := NewSignetBase(tool.Definition())
newSignet.ID = signetID
// generate signet and log time taken
start := time.Now()
err = tool.GenerateKey(newSignet)
if err != nil {
return nil, err
}
t.Logf("generated %s signet %s in %s", newSignet.Scheme, newSignet.ID, time.Since(start))
// store signet
err = testTrustStore.StoreSignet(newSignet)
if err != nil {
return nil, err
}
// store recipient
newRcpt, err := newSignet.AsRecipient()
if err != nil {
return nil, err
}
err = testTrustStore.StoreSignet(newRcpt)
if err != nil {
return nil, err
}
// return
if recipient {
return newRcpt, nil
}
return newSignet, nil
}
// generateCombinations returns all possible combinations of the given []string slice.
//
// Forked from https://github.com/mxschmitt/golang-combinations/blob/a887187146560effd2677e987b069262f356297f/combinations.go
// Copyright (c) 2018 Max Schmitt,
// MIT License.
func generateCombinations(set []string) (subsets [][]string) {
length := uint(len(set))
// Go through all possible combinations of objects
// from 1 (only first object in subset) to 2^length (all objects in subset)
for subsetBits := 1; subsetBits < (1 << length); subsetBits++ {
var subset []string
for object := uint(0); object < length; object++ {
// checks if object is contained in subset
// by checking if bit 'object' is set in subsetBits
if (subsetBits>>object)&1 == 1 {
// add object to subset
subset = append(subset, set[object])
}
}
// add subset to subsets
subsets = append(subsets, subset)
}
return subsets
}