From 4c4b4471d8e6820b934b007035fbc53b707937d4 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 8 Nov 2024 14:28:09 +0100 Subject: [PATCH] Add support for json file signing --- filesig/json.go | 98 ++++++++++++++++++++++++++++++++--- filesig/json_test.go | 119 ++++++++++++++++++++++++++++++++++++++++--- filesig/main.go | 2 +- filesig/text_test.go | 21 ++++---- 4 files changed, 215 insertions(+), 25 deletions(-) diff --git a/filesig/json.go b/filesig/json.go index 84906a0..7ec1c10 100644 --- a/filesig/json.go +++ b/filesig/json.go @@ -1,6 +1,7 @@ package filesig import ( + "encoding/base64" "errors" "fmt" @@ -9,7 +10,9 @@ import ( "github.com/tidwall/sjson" "golang.org/x/exp/slices" + "github.com/safing/jess" "github.com/safing/jess/lhash" + "github.com/safing/structures/dsd" ) // JSON file metadata keys. @@ -32,10 +35,10 @@ func AddJSONChecksum(data []byte) ([]byte, error) { checksums = append(checksums, h.Base58()) // Sort and deduplicate checksums and sigs. - slices.Sort[[]string, string](checksums) - checksums = slices.Compact[[]string, string](checksums) - slices.Sort[[]string, string](signatures) - signatures = slices.Compact[[]string, string](signatures) + slices.Sort(checksums) + checksums = slices.Compact(checksums) + slices.Sort(signatures) + signatures = slices.Compact(signatures) // Add metadata and return. return jsonAddMeta(content, checksums, signatures) @@ -72,6 +75,86 @@ func VerifyJSONChecksum(data []byte) error { return nil } +func AddJSONSignature(data []byte, envelope *jess.Envelope, trustStore jess.TrustStore) (signedData []byte, err error) { + // Create session. + session, err := envelope.Correspondence(trustStore) + if err != nil { + return nil, fmt.Errorf("invalid signing envelope: %w", err) + } + + // Check if the envelope is suitable for signing. + if err := envelope.Suite().Provides.CheckComplianceTo(fileSigRequirements); err != nil { + return nil, fmt.Errorf("envelope not suitable for signing: %w", err) + } + + // Extract content and metadata from json. + content, checksums, signatures, err := jsonSplit(data) + if err != nil { + return nil, fmt.Errorf("invalid json structure: %w", err) + } + + // Sign data. + letter, err := session.Close(content) + if err != nil { + return nil, fmt.Errorf("sign: %w", err) + } + + // Serialize signature and add it. + letter.Data = nil + sig, err := letter.ToDSD(dsd.CBOR) + if err != nil { + return nil, fmt.Errorf("serialize sig: %w", err) + } + signatures = append(signatures, base64.RawURLEncoding.EncodeToString(sig)) + + // Sort and deduplicate checksums and sigs. + slices.Sort(checksums) + checksums = slices.Compact(checksums) + slices.Sort(signatures) + signatures = slices.Compact(signatures) + + // Add metadata and return. + return jsonAddMeta(data, checksums, signatures) +} + +func VerifyJSONSignature(data []byte, trustStore jess.TrustStore) (err error) { + // Extract content and metadata from json. + content, _, signatures, err := jsonSplit(data) + if err != nil { + return fmt.Errorf("invalid json structure: %w", err) + } + + var signaturesVerified int + for i, sig := range signatures { + // Deserialize signature. + sigData, err := base64.RawURLEncoding.DecodeString(sig) + if err != nil { + return fmt.Errorf("signature %d malformed: %w", i+1, err) + } + letter := &jess.Letter{} + _, err = dsd.Load(sigData, letter) + if err != nil { + return fmt.Errorf("signature %d malformed: %w", i+1, err) + } + + // Verify signature. + letter.Data = content + err = letter.Verify(fileSigRequirements, trustStore) + if err != nil { + return fmt.Errorf("signature %d invalid: %w", i+1, err) + } + + signaturesVerified++ + } + + // Fail when no signatures were verified. + if signaturesVerified == 0 { + return ErrSignatureMissing + } + + return nil +} + func jsonSplit(data []byte) ( content []byte, checksums []string, @@ -187,10 +270,9 @@ func jsonAddMeta(data []byte, checksums, signatures []string) ([]byte, error) { // Final pretty print. data = pretty.PrettyOptions(data, &pretty.Options{ - Width: 200, // Must not change! - Prefix: "", // Must not change! - Indent: " ", // Must not change! - SortKeys: true, // Must not change! + Width: 200, // Must not change! + Prefix: "", // Must not change! + Indent: " ", // Must not change! }) return data, nil diff --git a/filesig/json_test.go b/filesig/json_test.go index a1505f7..58d3e03 100644 --- a/filesig/json_test.go +++ b/filesig/json_test.go @@ -4,6 +4,10 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/safing/jess" + "github.com/safing/jess/tools" ) func TestJSONChecksums(t *testing.T) { @@ -22,9 +26,9 @@ func TestJSONChecksums(t *testing.T) { ` testJSONWithChecksum, err := AddJSONChecksum([]byte(json)) - assert.NoError(t, err, "should be able to add checksum") + require.NoError(t, err, "should be able to add checksum") assert.Equal(t, jsonWithChecksum, string(testJSONWithChecksum), "should match") - assert.NoError(t, + require.NoError(t, VerifyJSONChecksum(testJSONWithChecksum), "checksum should be correct", ) @@ -33,7 +37,7 @@ func TestJSONChecksums(t *testing.T) { "c": 1, "a":"b", "_jess-checksum": "ZwtAd75qvioh6uf1NAq64KRgTbqeehFVYmhLmrwu1s7xJo" }` - assert.NoError(t, + require.NoError(t, VerifyJSONChecksum([]byte(jsonWithChecksum)), "checksum should be correct", ) @@ -48,7 +52,7 @@ func TestJSONChecksums(t *testing.T) { "c": 1 } ` - assert.NoError(t, + require.NoError(t, VerifyJSONChecksum([]byte(jsonWithMultiChecksum)), "checksum should be correct", ) @@ -61,9 +65,9 @@ func TestJSONChecksums(t *testing.T) { ` testJSONWithMultiChecksum, err := AddJSONChecksum([]byte(jsonWithMultiChecksum)) - assert.NoError(t, err, "should be able to add checksum") + require.NoError(t, err, "should be able to add checksum") assert.Equal(t, jsonWithMultiChecksumOutput, string(testJSONWithMultiChecksum), "should match") - assert.NoError(t, + require.NoError(t, VerifyJSONChecksum(testJSONWithMultiChecksum), "checksum should be correct", ) @@ -117,3 +121,106 @@ func TestJSONChecksums(t *testing.T) { // // assert.Error(t, VerifyTextFileChecksum([]byte(textWithFailingChecksums), "#"), "should fail") } + +func TestJSONSignatures(t *testing.T) { + t.Parallel() + + // Get tool for key generation. + tool, err := tools.Get("Ed25519") + if err != nil { + t.Fatal(err) + } + + // Generate key pair. + s, err := getOrMakeSignet(t, tool.StaticLogic, false, "test-key-jsonsig-1") + if err != nil { + t.Fatal(err) + } + // sBackup, err := s.Backup(true) + // if err != nil { + // t.Fatal(err) + // } + // t.Logf("signet: %s", sBackup) + + // Make envelope. + envelope := jess.NewUnconfiguredEnvelope() + envelope.SuiteID = jess.SuiteSignV1 + envelope.Senders = []*jess.Signet{s} + + // Test 1: Simple json. + + json := `{"a": "b", "c": 1}` + testJSONWithSignature, err := AddJSONSignature([]byte(json), envelope, testTrustStore) + require.NoError(t, err, "should be able to add signature") + require.NoError(t, + VerifyJSONSignature(testJSONWithSignature, testTrustStore), + "signature should be valid", + ) + + // Test 2: Prepared json with signature. + + // Load signing key into trust store. + signingKey2, err := jess.SenderFromTextFormat( + "sender:2ZxXzzL3mc3mLPizTUe49zi8Z3NMbDrmmqJ4V9mL4AxefZ1o8pM8wPMuK2uW12Mvd3EJL9wsKTn14BDuqH2AtucvHTAkjDdZZ5YA9Azmji5tLRXmypvSxEj2mxXU3MFXBVdpzPdwRcE4WauLo9ZfQWebznvnatVLwuxmeo17tU2pL7", + ) + if err != nil { + t.Fatal(err) + } + rcptKey2, err := signingKey2.AsRecipient() + if err != nil { + t.Fatal(err) + } + if err := testTrustStore.StoreSignet(rcptKey2); err != nil { + t.Fatal(err) + } + + // Verify data. + jsonWithSignature := `{ + "c":1,"a":"b", + "_jess-signature": "Q6RnVmVyc2lvbgFnU3VpdGVJRGdzaWduX3YxZU5vbmNlRK6e7JhqU2lnbmF0dXJlc4GjZlNjaGVtZWdFZDI1NTE5YklEeBl0ZXN0LXN0YXRpYy1rZXktanNvbnNpZy0xZVZhbHVlWEBPEbeM4_CTl3OhNT2z74h38jIZG5R7BBLDFd6npJ3E-4JqM6TaSMa-2pPEBf3fDNuikR3ak45SekC6Z10uWiEB" + }` + require.NoError(t, + VerifyJSONSignature([]byte(jsonWithSignature), testTrustStore), + "signature should be valid", + ) + + // Test 3: Add signature to prepared json. + + testJSONWithSignature, err = AddJSONSignature([]byte(jsonWithSignature), envelope, testTrustStore) + require.NoError(t, err, "should be able to add signature") + require.NoError(t, + VerifyJSONSignature(testJSONWithSignature, testTrustStore), + "signatures should be valid", + ) + + // Test 4: Prepared json with multiple signatures. + + // Load signing key into trust store. + signingKey3, err := jess.SenderFromTextFormat( + "sender:2ZxXzzL3mc3mLPizTUe49zi8Z3NMbDrmmqJ4V9mL4AxefZ1o8pM8wPMuRAXdZNaPX3B96bhGCpww6TbXJ6WXLHoLwLV196cgdm1BurfTMdjUPa4PUj1KgHuM82b1p8ezQeryzj1CsjeM8KRQdh9YP87gwKpXNmLW5GmUyWG5KxzZ7W", + ) + if err != nil { + t.Fatal(err) + } + rcptKey3, err := signingKey3.AsRecipient() + if err != nil { + t.Fatal(err) + } + if err := testTrustStore.StoreSignet(rcptKey3); err != nil { + t.Fatal(err) + } + + jsonWithMultiSig := `{ + "_jess-signature": [ + "Q6RnVmVyc2lvbgFnU3VpdGVJRGdzaWduX3YxZU5vbmNlRK6e7JhqU2lnbmF0dXJlc4GjZlNjaGVtZWdFZDI1NTE5YklEeBl0ZXN0LXN0YXRpYy1rZXktanNvbnNpZy0xZVZhbHVlWEBPEbeM4_CTl3OhNT2z74h38jIZG5R7BBLDFd6npJ3E-4JqM6TaSMa-2pPEBf3fDNuikR3ak45SekC6Z10uWiEB", + "Q6RnVmVyc2lvbgFnU3VpdGVJRGdzaWduX3YxZU5vbmNlRC32oylqU2lnbmF0dXJlc4GjZlNjaGVtZWdFZDI1NTE5YklEeBl0ZXN0LXN0YXRpYy1rZXktanNvbnNpZy0yZVZhbHVlWEDYVHeKaJvzZPOkgC6Tie6x70bNm2jtmJmAwDFDcBL1ddK7pVSefyAPg47xMO7jeucP5bw754P6CdrR5gyANJkM" + ], + "a": "b", + "c": 1 + } + ` + assert.NoError(t, + VerifyJSONSignature([]byte(jsonWithMultiSig), testTrustStore), + "signatures should be valid", + ) +} diff --git a/filesig/main.go b/filesig/main.go index 9934422..49c7027 100644 --- a/filesig/main.go +++ b/filesig/main.go @@ -53,7 +53,7 @@ func SignFileData(fileHash *lhash.LabeledHash, metaData map[string]string, envel // Check if the envelope is suitable for signing. if err := envelope.Suite().Provides.CheckComplianceTo(fileSigRequirements); err != nil { - return nil, nil, fmt.Errorf("envelope not suitable for signing") + return nil, nil, fmt.Errorf("envelope not suitable for signing: %w", err) } // Create struct and transform data into serializable format to be signed. diff --git a/filesig/text_test.go b/filesig/text_test.go index e896c75..9d69e56 100644 --- a/filesig/text_test.go +++ b/filesig/text_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTextChecksums(t *testing.T) { @@ -29,20 +30,20 @@ do_something() ` testTextWithChecksumAfterComment, err := AddTextFileChecksum([]byte(text), "#", TextPlacementAfterComment) - assert.NoError(t, err, "should be able to add checksum") + require.NoError(t, err, "should be able to add checksum") assert.Equal(t, textWithChecksumAfterComment, string(testTextWithChecksumAfterComment), "should match") - assert.NoError(t, + require.NoError(t, VerifyTextFileChecksum(testTextWithChecksumAfterComment, "#"), "checksum should be correct", ) - assert.NoError(t, + require.NoError(t, VerifyTextFileChecksum(append( []byte("\n\n \r\n"), testTextWithChecksumAfterComment..., ), "#"), "checksum should be correct", ) - assert.NoError(t, + require.NoError(t, VerifyTextFileChecksum(append( testTextWithChecksumAfterComment, []byte("\r\n \n \n")..., @@ -62,9 +63,9 @@ do_something() ` testTextWithChecksumAtTop, err := AddTextFileChecksum([]byte(text), "#", TextPlacementTop) - assert.NoError(t, err, "should be able to add checksum") + require.NoError(t, err, "should be able to add checksum") assert.Equal(t, textWithChecksumAtTop, string(testTextWithChecksumAtTop), "should match") - assert.NoError(t, + require.NoError(t, VerifyTextFileChecksum(testTextWithChecksumAtTop, "#"), "checksum should be correct", ) @@ -82,9 +83,9 @@ do_something() ` testTextWithChecksumAtBottom, err := AddTextFileChecksum([]byte(text), "#", TextPlacementBottom) - assert.NoError(t, err, "should be able to add checksum") + require.NoError(t, err, "should be able to add checksum") assert.Equal(t, textWithChecksumAtBottom, string(testTextWithChecksumAtBottom), "should match") - assert.NoError(t, + require.NoError(t, VerifyTextFileChecksum(testTextWithChecksumAtBottom, "#"), "checksum should be correct", ) @@ -119,7 +120,7 @@ do_something() do_something() ` testTextWithMultiChecksumOutput, err := AddTextFileChecksum([]byte(textWithMultiChecksum), "#", TextPlacementAfterComment) - assert.NoError(t, err, "should be able to add checksum") + require.NoError(t, err, "should be able to add checksum") assert.Equal(t, textWithMultiChecksumOutput, string(testTextWithMultiChecksumOutput), "should match") // Test failing checksums. @@ -135,7 +136,7 @@ do_something() do_something() ` - assert.Error(t, VerifyTextFileChecksum([]byte(textWithFailingChecksums), "#"), "should fail") + require.Error(t, VerifyTextFileChecksum([]byte(textWithFailingChecksums), "#"), "should fail") } func TestLineEndDetection(t *testing.T) {