Skip to content

Commit

Permalink
Add support for embedded text, json and yaml checksums
Browse files Browse the repository at this point in the history
  • Loading branch information
dhaavi committed Sep 26, 2023
1 parent ed928f9 commit 9c7f695
Show file tree
Hide file tree
Showing 7 changed files with 850 additions and 1 deletion.
111 changes: 111 additions & 0 deletions cmd/cmd-checksum.go
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
}
197 changes: 197 additions & 0 deletions filesig/json.go
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
}
Loading

0 comments on commit 9c7f695

Please sign in to comment.