-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support for embedded text, json and yaml checksums
- Loading branch information
Showing
7 changed files
with
850 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
package main | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
"os" | ||
"path/filepath" | ||
|
||
"github.com/spf13/cobra" | ||
|
||
"github.com/safing/jess/filesig" | ||
) | ||
|
||
func init() { | ||
rootCmd.AddCommand(checksum) | ||
checksum.AddCommand(checksumAdd) | ||
checksum.AddCommand(checksumVerify) | ||
} | ||
|
||
var ( | ||
checksum = &cobra.Command{ | ||
Use: "checksum", | ||
Short: "add or verify embedded checksums", | ||
} | ||
|
||
checksumAddUsage = "usage: checksum add <file>" | ||
checksumAdd = &cobra.Command{ | ||
Use: "add <file>", | ||
Short: "add an embedded checksum to a file", | ||
Long: "add an embedded checksum to a file (support file types: txt, json, yaml)", | ||
RunE: handleChecksumAdd, | ||
} | ||
|
||
checksumVerifyUsage = "usage: checksum verify <file>" | ||
checksumVerify = &cobra.Command{ | ||
Use: "verify <file>", | ||
Short: "verify the embedded checksum of a file", | ||
Long: "verify the embedded checksum of a file (support file types: txt, json, yaml)", | ||
RunE: handleChecksumVerify, | ||
} | ||
) | ||
|
||
func handleChecksumAdd(cmd *cobra.Command, args []string) error { | ||
// Check args. | ||
if len(args) != 1 { | ||
return errors.New(checksumAddUsage) | ||
} | ||
filename := args[0] | ||
|
||
data, err := os.ReadFile(filename) | ||
if err != nil { | ||
return fmt.Errorf("failed to read file: %w", err) | ||
} | ||
|
||
switch filepath.Ext(filename) { | ||
case ".json": | ||
data, err = filesig.AddJSONChecksum(data) | ||
case ".yml", ".yaml": | ||
data, err = filesig.AddYAMLChecksum(data, filesig.TextPlacementAfterComment) | ||
case ".txt": | ||
data, err = filesig.AddTextFileChecksum(data, "#", filesig.TextPlacementAfterComment) | ||
default: | ||
return errors.New("unsupported file format") | ||
} | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Write back to disk. | ||
fileInfo, err := os.Stat(filename) | ||
if err != nil { | ||
return fmt.Errorf("failed to stat file: %w", err) | ||
} | ||
err = os.WriteFile(filename, data, fileInfo.Mode().Perm()) | ||
if err != nil { | ||
return fmt.Errorf("failed to write back file with checksum: %w", err) | ||
} | ||
|
||
fmt.Println("checksum added") | ||
return nil | ||
} | ||
|
||
func handleChecksumVerify(cmd *cobra.Command, args []string) error { | ||
// Check args. | ||
if len(args) != 1 { | ||
return errors.New(checksumVerifyUsage) | ||
} | ||
filename := args[0] | ||
|
||
data, err := os.ReadFile(filename) | ||
if err != nil { | ||
return fmt.Errorf("failed to read file: %w", err) | ||
} | ||
|
||
switch filepath.Ext(filename) { | ||
case ".json": | ||
err = filesig.VerifyJSONChecksum(data) | ||
case ".yml", ".yaml": | ||
err = filesig.VerifyYAMLChecksum(data) | ||
case ".txt": | ||
err = filesig.VerifyTextFileChecksum(data, "#") | ||
default: | ||
return errors.New("unsupported file format") | ||
} | ||
if err != nil { | ||
return err | ||
} | ||
|
||
fmt.Println("checksum verified") | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
package filesig | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
|
||
"github.com/tidwall/gjson" | ||
"github.com/tidwall/pretty" | ||
"github.com/tidwall/sjson" | ||
"golang.org/x/exp/slices" | ||
|
||
"github.com/safing/jess/lhash" | ||
) | ||
|
||
// JSON file metadata keys. | ||
const ( | ||
JSONKeyPrefix = "_jess-" | ||
JSONChecksumKey = JSONKeyPrefix + "checksum" | ||
JSONSignatureKey = JSONKeyPrefix + "signature" | ||
) | ||
|
||
// AddJSONChecksum adds a checksum to a text file. | ||
func AddJSONChecksum(data []byte) ([]byte, error) { | ||
// Extract content and metadata from json. | ||
content, checksums, signatures, err := jsonSplit(data) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Calculate checksum. | ||
h := lhash.BLAKE2b_256.Digest(content) | ||
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) | ||
|
||
// Add metadata and return. | ||
return jsonAddMeta(content, checksums, signatures) | ||
} | ||
|
||
// VerifyJSONChecksum checks a checksum in a text file. | ||
func VerifyJSONChecksum(data []byte) error { | ||
// Extract content and metadata from json. | ||
content, checksums, _, err := jsonSplit(data) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
// Verify all checksums. | ||
var checksumsVerified int | ||
for _, checksum := range checksums { | ||
// Parse checksum. | ||
h, err := lhash.FromBase58(checksum) | ||
if err != nil { | ||
return fmt.Errorf("%w: failed to parse labeled hash: %w", ErrChecksumFailed, err) | ||
} | ||
// Verify checksum. | ||
if !h.Matches(content) { | ||
return ErrChecksumFailed | ||
} | ||
checksumsVerified++ | ||
} | ||
|
||
// Fail when no checksums were verified. | ||
if checksumsVerified == 0 { | ||
return ErrChecksumMissing | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func jsonSplit(data []byte) ( | ||
content []byte, | ||
checksums []string, | ||
signatures []string, | ||
err error, | ||
) { | ||
// Check json. | ||
if !gjson.ValidBytes(data) { | ||
return nil, nil, nil, errors.New("invalid json") | ||
} | ||
content = data | ||
|
||
// Get checksums. | ||
result := gjson.GetBytes(content, JSONChecksumKey) | ||
if result.Exists() { | ||
if result.IsArray() { | ||
array := result.Array() | ||
checksums = make([]string, 0, len(array)) | ||
for _, result := range array { | ||
if result.Type == gjson.String { | ||
checksums = append(checksums, result.String()) | ||
} | ||
} | ||
} else if result.Type == gjson.String { | ||
checksums = []string{result.String()} | ||
} | ||
|
||
// Delete key. | ||
content, err = sjson.DeleteBytes(content, JSONChecksumKey) | ||
if err != nil { | ||
return nil, nil, nil, err | ||
} | ||
} | ||
|
||
// Get signatures. | ||
result = gjson.GetBytes(content, JSONSignatureKey) | ||
if result.Exists() { | ||
if result.IsArray() { | ||
array := result.Array() | ||
signatures = make([]string, 0, len(array)) | ||
for _, result := range array { | ||
if result.Type == gjson.String { | ||
signatures = append(signatures, result.String()) | ||
} | ||
} | ||
} else if result.Type == gjson.String { | ||
signatures = []string{result.String()} | ||
} | ||
|
||
// Delete key. | ||
content, err = sjson.DeleteBytes(content, JSONSignatureKey) | ||
if err != nil { | ||
return nil, nil, nil, err | ||
} | ||
} | ||
|
||
// Format for reproducible checksums and signatures. | ||
content = pretty.PrettyOptions(content, &pretty.Options{ | ||
Width: 200, // Must not change! | ||
Prefix: "", // Must not change! | ||
Indent: " ", // Must not change! | ||
SortKeys: true, // Must not change! | ||
}) | ||
|
||
return content, checksums, signatures, nil | ||
} | ||
|
||
func jsonAddMeta(data []byte, checksums, signatures []string) ([]byte, error) { | ||
var ( | ||
err error | ||
opts = &sjson.Options{ | ||
ReplaceInPlace: true, | ||
} | ||
) | ||
|
||
// Add checksums. | ||
switch len(checksums) { | ||
case 0: | ||
// Skip | ||
case 1: | ||
// Add single checksum. | ||
data, err = sjson.SetBytesOptions( | ||
data, JSONChecksumKey, checksums[0], opts, | ||
) | ||
default: | ||
// Add multiple checksums. | ||
data, err = sjson.SetBytesOptions( | ||
data, JSONChecksumKey, checksums, opts, | ||
) | ||
} | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// Add signatures. | ||
switch len(signatures) { | ||
case 0: | ||
// Skip | ||
case 1: | ||
// Add single signature. | ||
data, err = sjson.SetBytesOptions( | ||
data, JSONSignatureKey, signatures[0], opts, | ||
) | ||
default: | ||
// Add multiple signatures. | ||
data, err = sjson.SetBytesOptions( | ||
data, JSONSignatureKey, signatures, opts, | ||
) | ||
} | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
// 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! | ||
}) | ||
|
||
return data, nil | ||
} |
Oops, something went wrong.