Add support for json file signing

This commit is contained in:
Daniel 2024-11-08 14:28:09 +01:00
parent 4fbce7d649
commit 4c4b4471d8
4 changed files with 215 additions and 25 deletions

View file

@ -1,6 +1,7 @@
package filesig package filesig
import ( import (
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
@ -9,7 +10,9 @@ import (
"github.com/tidwall/sjson" "github.com/tidwall/sjson"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"github.com/safing/jess"
"github.com/safing/jess/lhash" "github.com/safing/jess/lhash"
"github.com/safing/structures/dsd"
) )
// JSON file metadata keys. // JSON file metadata keys.
@ -32,10 +35,10 @@ func AddJSONChecksum(data []byte) ([]byte, error) {
checksums = append(checksums, h.Base58()) checksums = append(checksums, h.Base58())
// Sort and deduplicate checksums and sigs. // Sort and deduplicate checksums and sigs.
slices.Sort[[]string, string](checksums) slices.Sort(checksums)
checksums = slices.Compact[[]string, string](checksums) checksums = slices.Compact(checksums)
slices.Sort[[]string, string](signatures) slices.Sort(signatures)
signatures = slices.Compact[[]string, string](signatures) signatures = slices.Compact(signatures)
// Add metadata and return. // Add metadata and return.
return jsonAddMeta(content, checksums, signatures) return jsonAddMeta(content, checksums, signatures)
@ -72,6 +75,86 @@ func VerifyJSONChecksum(data []byte) error {
return nil 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) ( func jsonSplit(data []byte) (
content []byte, content []byte,
checksums []string, checksums []string,
@ -187,10 +270,9 @@ func jsonAddMeta(data []byte, checksums, signatures []string) ([]byte, error) {
// Final pretty print. // Final pretty print.
data = pretty.PrettyOptions(data, &pretty.Options{ data = pretty.PrettyOptions(data, &pretty.Options{
Width: 200, // Must not change! Width: 200, // Must not change!
Prefix: "", // Must not change! Prefix: "", // Must not change!
Indent: " ", // Must not change! Indent: " ", // Must not change!
SortKeys: true, // Must not change!
}) })
return data, nil return data, nil

View file

@ -4,6 +4,10 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/safing/jess"
"github.com/safing/jess/tools"
) )
func TestJSONChecksums(t *testing.T) { func TestJSONChecksums(t *testing.T) {
@ -22,9 +26,9 @@ func TestJSONChecksums(t *testing.T) {
` `
testJSONWithChecksum, err := AddJSONChecksum([]byte(json)) 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.Equal(t, jsonWithChecksum, string(testJSONWithChecksum), "should match")
assert.NoError(t, require.NoError(t,
VerifyJSONChecksum(testJSONWithChecksum), VerifyJSONChecksum(testJSONWithChecksum),
"checksum should be correct", "checksum should be correct",
) )
@ -33,7 +37,7 @@ func TestJSONChecksums(t *testing.T) {
"c": 1, "a":"b", "c": 1, "a":"b",
"_jess-checksum": "ZwtAd75qvioh6uf1NAq64KRgTbqeehFVYmhLmrwu1s7xJo" "_jess-checksum": "ZwtAd75qvioh6uf1NAq64KRgTbqeehFVYmhLmrwu1s7xJo"
}` }`
assert.NoError(t, require.NoError(t,
VerifyJSONChecksum([]byte(jsonWithChecksum)), VerifyJSONChecksum([]byte(jsonWithChecksum)),
"checksum should be correct", "checksum should be correct",
) )
@ -48,7 +52,7 @@ func TestJSONChecksums(t *testing.T) {
"c": 1 "c": 1
} }
` `
assert.NoError(t, require.NoError(t,
VerifyJSONChecksum([]byte(jsonWithMultiChecksum)), VerifyJSONChecksum([]byte(jsonWithMultiChecksum)),
"checksum should be correct", "checksum should be correct",
) )
@ -61,9 +65,9 @@ func TestJSONChecksums(t *testing.T) {
` `
testJSONWithMultiChecksum, err := AddJSONChecksum([]byte(jsonWithMultiChecksum)) 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.Equal(t, jsonWithMultiChecksumOutput, string(testJSONWithMultiChecksum), "should match")
assert.NoError(t, require.NoError(t,
VerifyJSONChecksum(testJSONWithMultiChecksum), VerifyJSONChecksum(testJSONWithMultiChecksum),
"checksum should be correct", "checksum should be correct",
) )
@ -117,3 +121,106 @@ func TestJSONChecksums(t *testing.T) {
// //
// assert.Error(t, VerifyTextFileChecksum([]byte(textWithFailingChecksums), "#"), "should fail") // 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",
)
}

View file

@ -53,7 +53,7 @@ func SignFileData(fileHash *lhash.LabeledHash, metaData map[string]string, envel
// Check if the envelope is suitable for signing. // Check if the envelope is suitable for signing.
if err := envelope.Suite().Provides.CheckComplianceTo(fileSigRequirements); err != nil { 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. // Create struct and transform data into serializable format to be signed.

View file

@ -4,6 +4,7 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestTextChecksums(t *testing.T) { func TestTextChecksums(t *testing.T) {
@ -29,20 +30,20 @@ do_something()
` `
testTextWithChecksumAfterComment, err := AddTextFileChecksum([]byte(text), "#", TextPlacementAfterComment) 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.Equal(t, textWithChecksumAfterComment, string(testTextWithChecksumAfterComment), "should match")
assert.NoError(t, require.NoError(t,
VerifyTextFileChecksum(testTextWithChecksumAfterComment, "#"), VerifyTextFileChecksum(testTextWithChecksumAfterComment, "#"),
"checksum should be correct", "checksum should be correct",
) )
assert.NoError(t, require.NoError(t,
VerifyTextFileChecksum(append( VerifyTextFileChecksum(append(
[]byte("\n\n \r\n"), []byte("\n\n \r\n"),
testTextWithChecksumAfterComment..., testTextWithChecksumAfterComment...,
), "#"), ), "#"),
"checksum should be correct", "checksum should be correct",
) )
assert.NoError(t, require.NoError(t,
VerifyTextFileChecksum(append( VerifyTextFileChecksum(append(
testTextWithChecksumAfterComment, testTextWithChecksumAfterComment,
[]byte("\r\n \n \n")..., []byte("\r\n \n \n")...,
@ -62,9 +63,9 @@ do_something()
` `
testTextWithChecksumAtTop, err := AddTextFileChecksum([]byte(text), "#", TextPlacementTop) 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.Equal(t, textWithChecksumAtTop, string(testTextWithChecksumAtTop), "should match")
assert.NoError(t, require.NoError(t,
VerifyTextFileChecksum(testTextWithChecksumAtTop, "#"), VerifyTextFileChecksum(testTextWithChecksumAtTop, "#"),
"checksum should be correct", "checksum should be correct",
) )
@ -82,9 +83,9 @@ do_something()
` `
testTextWithChecksumAtBottom, err := AddTextFileChecksum([]byte(text), "#", TextPlacementBottom) 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.Equal(t, textWithChecksumAtBottom, string(testTextWithChecksumAtBottom), "should match")
assert.NoError(t, require.NoError(t,
VerifyTextFileChecksum(testTextWithChecksumAtBottom, "#"), VerifyTextFileChecksum(testTextWithChecksumAtBottom, "#"),
"checksum should be correct", "checksum should be correct",
) )
@ -119,7 +120,7 @@ do_something()
do_something() do_something()
` `
testTextWithMultiChecksumOutput, err := AddTextFileChecksum([]byte(textWithMultiChecksum), "#", TextPlacementAfterComment) 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") assert.Equal(t, textWithMultiChecksumOutput, string(testTextWithMultiChecksumOutput), "should match")
// Test failing checksums. // Test failing checksums.
@ -135,7 +136,7 @@ do_something()
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) { func TestLineEndDetection(t *testing.T) {