Add filesig package for signing files in a common way
This commit is contained in:
parent
8ad0eabf82
commit
0bb5f33c4a
5 changed files with 681 additions and 0 deletions
123
filesig/format_armor.go
Normal file
123
filesig/format_armor.go
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
package jess
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/safing/jess"
|
||||||
|
"github.com/safing/portbase/formats/dsd"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
sigFileArmorStart = "-----BEGIN JESS SIGNATURE-----"
|
||||||
|
sigFileArmorEnd = "-----END JESS SIGNATURE-----"
|
||||||
|
sigFileLineLength = 64
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
sigFileArmorFindMatcher = regexp.MustCompile(`(?ms)` + sigFileArmorStart + `(.+?)` + sigFileArmorEnd)
|
||||||
|
sigFileArmorRemoveMatcher = regexp.MustCompile(`(?ms)` + sigFileArmorStart + `.+?` + sigFileArmorEnd + `\r?\n?`)
|
||||||
|
whitespaceMatcher = regexp.MustCompile(`(?ms)\s`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseSigFile parses a signature file and extracts any jess signatures from it.
|
||||||
|
// If signatures are returned along with an error, the error should be treated
|
||||||
|
// as a warning, but the result should also not be treated as a full success,
|
||||||
|
// as there might be missing signatures.
|
||||||
|
func ParseSigFile(fileData []byte) (signatures []*jess.Letter, err error) {
|
||||||
|
var warning error
|
||||||
|
captured := make([][]byte, 0, 1)
|
||||||
|
|
||||||
|
// Find any signature blocks.
|
||||||
|
matches := sigFileArmorFindMatcher.FindAllSubmatch(fileData, -1)
|
||||||
|
for _, subMatches := range matches {
|
||||||
|
if len(subMatches) >= 2 {
|
||||||
|
// First entry is the whole match, second the submatch.
|
||||||
|
captured = append(
|
||||||
|
captured,
|
||||||
|
bytes.TrimPrefix(
|
||||||
|
bytes.TrimSuffix(
|
||||||
|
whitespaceMatcher.ReplaceAll(subMatches[1], nil),
|
||||||
|
[]byte(sigFileArmorEnd),
|
||||||
|
),
|
||||||
|
[]byte(sigFileArmorStart),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse any found signatures.
|
||||||
|
signatures = make([]*jess.Letter, 0, len(captured))
|
||||||
|
for _, sigBase64Data := range captured {
|
||||||
|
// Decode from base64
|
||||||
|
sigData := make([]byte, base64.RawStdEncoding.DecodedLen(len(sigBase64Data)))
|
||||||
|
_, err = base64.RawStdEncoding.Decode(sigData, sigBase64Data)
|
||||||
|
if err != nil {
|
||||||
|
warning = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse signature.
|
||||||
|
var letter *jess.Letter
|
||||||
|
letter, err = jess.LetterFromDSD(sigData)
|
||||||
|
if err != nil {
|
||||||
|
warning = err
|
||||||
|
} else {
|
||||||
|
signatures = append(signatures, letter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return signatures, warning
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeSigFileSection creates a new section for a signature file.
|
||||||
|
func MakeSigFileSection(signature *jess.Letter) ([]byte, error) {
|
||||||
|
// Serialize.
|
||||||
|
data, err := signature.ToDSD(dsd.CBOR)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to serialize signature: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode to base64
|
||||||
|
encodedData := make([]byte, base64.RawStdEncoding.EncodedLen(len(data)))
|
||||||
|
base64.RawStdEncoding.Encode(encodedData, data)
|
||||||
|
|
||||||
|
// Split into lines and add armor.
|
||||||
|
splittedData := make([][]byte, 0, (len(encodedData)/sigFileLineLength)+3)
|
||||||
|
splittedData = append(splittedData, []byte(sigFileArmorStart))
|
||||||
|
for len(encodedData) > 0 {
|
||||||
|
if len(encodedData) > sigFileLineLength {
|
||||||
|
splittedData = append(splittedData, encodedData[:sigFileLineLength])
|
||||||
|
encodedData = encodedData[sigFileLineLength:]
|
||||||
|
} else {
|
||||||
|
splittedData = append(splittedData, encodedData)
|
||||||
|
encodedData = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
splittedData = append(splittedData, []byte(sigFileArmorEnd))
|
||||||
|
linedData := bytes.Join(splittedData, []byte("\n"))
|
||||||
|
|
||||||
|
return linedData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddToSigFile adds the given signature to the signature file.
|
||||||
|
func AddToSigFile(signature *jess.Letter, sigFileData []byte, removeExistingJessSignatures bool) (newFileData []byte, err error) {
|
||||||
|
// Create new section for new sig.
|
||||||
|
newSigSection, err := MakeSigFileSection(signature)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove any existing jess signature sections.
|
||||||
|
if removeExistingJessSignatures {
|
||||||
|
sigFileData = sigFileArmorRemoveMatcher.ReplaceAll(sigFileData, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append new signature section to end of file with a newline.
|
||||||
|
sigFileData = append(sigFileData, []byte("\n")...)
|
||||||
|
sigFileData = append(sigFileData, newSigSection...)
|
||||||
|
|
||||||
|
return sigFileData, nil
|
||||||
|
}
|
197
filesig/format_armor_test.go
Normal file
197
filesig/format_armor_test.go
Normal file
|
@ -0,0 +1,197 @@
|
||||||
|
package jess
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/safing/jess"
|
||||||
|
"github.com/safing/jess/lhash"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testFileSigOneKey = "7KoUBdrRfF6drrPvKianoGfEXTQFCS5wDbfQyc87VQnYApPckRS8SfrrmAXZhV1JgKfnh44ib9nydQVEDRJiZArV22RqMfPrJmQdoAsE7zuzPRSrku8yF7zfnEv46X5GsmgfdSDrFMdG7XJd3fdaxStYCXTYDS5R"
|
||||||
|
|
||||||
|
testFileSigOneData = []byte("The quick brown fox jumps over the lazy dog")
|
||||||
|
|
||||||
|
testFileSigOneMetaData = map[string]string{
|
||||||
|
"id": "resource/path",
|
||||||
|
"version": "0.0.1",
|
||||||
|
}
|
||||||
|
|
||||||
|
testFileSigOneSignature = []byte(`
|
||||||
|
-----BEGIN JESS SIGNATURE-----
|
||||||
|
Q6VnVmVyc2lvbgFnU3VpdGVJRGdzaWduX3YxZU5vbmNlRA40a/BkRGF0YVhqTYOr
|
||||||
|
TGFiZWxlZEhhc2jEIhkgAXGM7DXNPXlt0AAg4L/stHOtI0V9Bjt17/KcD/ouWKmo
|
||||||
|
U2lnbmVkQXTW/2LH/ueoTWV0YURhdGGComlkrXJlc291cmNlL3BhdGindmVyc2lv
|
||||||
|
bqUwLjAuMWpTaWduYXR1cmVzgaNmU2NoZW1lZ0VkMjU1MTliSURwZmlsZXNpZy10
|
||||||
|
ZXN0LWtleWVWYWx1ZVhA4b1kfIJF7do6OcJnemQ5mtj/ZyMFJWWTmD1W5KvkpZac
|
||||||
|
2AP5f+dDJhzWBHsoSXTCl6uA3DA3+RbABMYAZn6eDg
|
||||||
|
-----END JESS SIGNATURE-----
|
||||||
|
`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileSigFormat(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Load test key.
|
||||||
|
signet, err := jess.SignetFromBase58(testFileSigOneKey)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store signet.
|
||||||
|
if err := testTrustStore.StoreSignet(signet); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Store public key for verification.
|
||||||
|
recipient, err := signet.AsRecipient()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := testTrustStore.StoreSignet(recipient); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create envelope.
|
||||||
|
envelope := jess.NewUnconfiguredEnvelope()
|
||||||
|
envelope.SuiteID = jess.SuiteSignV1
|
||||||
|
envelope.Senders = []*jess.Signet{signet}
|
||||||
|
|
||||||
|
// Hash and sign file.
|
||||||
|
hash := lhash.Digest(lhash.BLAKE2b_256, testFileSigOneData)
|
||||||
|
letter, _, err := SignFileData(hash, testFileSigOneMetaData, envelope, testTrustStore)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize signature.
|
||||||
|
sigFile, err := MakeSigFileSection(letter)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// fmt.Println("Signature:")
|
||||||
|
// fmt.Println(string(sigFile))
|
||||||
|
|
||||||
|
// Parse signature again.
|
||||||
|
sigs, err := ParseSigFile(sigFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(sigs) != 1 {
|
||||||
|
t.Fatalf("one sig expected, got %d", len(sigs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Signature.
|
||||||
|
fileData, err := VerifyFileData(sigs[0], testFileSigOneMetaData, testTrustStore)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify File.
|
||||||
|
if !fileData.FileHash().MatchesData(testFileSigOneData) {
|
||||||
|
t.Fatal("file hash does not match")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the saved version of the signature.
|
||||||
|
|
||||||
|
// Parse the saved signature.
|
||||||
|
sigs, err = ParseSigFile(testFileSigOneSignature)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(sigs) != 1 {
|
||||||
|
t.Fatalf("only one sig expected, got %d", len(sigs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify Signature.
|
||||||
|
fileData, err = VerifyFileData(sigs[0], testFileSigOneMetaData, testTrustStore)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify File.
|
||||||
|
if !fileData.FileHash().MatchesData(testFileSigOneData) {
|
||||||
|
t.Fatal("file hash does not match")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
testFileSigFormat1 = []byte(`TGFiZWxlZEhhc2jEIhkgAXGM7DXNPXlt0AAg4L
|
||||||
|
-----BEGIN JESS SIGNATURE-----
|
||||||
|
Q6VnVmVyc2lvbgFnU3VpdGVJRGdzaWduX3YxZU5vbmNlRA40a/BkRGF0YVhqTYOr
|
||||||
|
TGFiZWxlZEhhc2jEIhkgAXGM7DXNPXlt0AAg4L/stHOtI0V9Bjt17/KcD/ouWKmo
|
||||||
|
U2lnbmVkQXTW/2LH/ueoTWV0YURhdGGComlkrXJlc291cmNlL3BhdGindmVyc2lv
|
||||||
|
bqUwLjAuMWpTaWduYXR1cmVzgaNmU2NoZW1lZ0VkMjU1MTliSURwZmlsZXNpZy10
|
||||||
|
ZXN0LWtleWVWYWx1ZVhA4b1kfIJF7do6OcJnemQ5mtj/ZyMFJWWTmD1W5KvkpZac
|
||||||
|
2AP5f+dDJhzWBHsoSXTCl6uA3DA3+RbABMYAZn6eDg
|
||||||
|
-----END JESS SIGNATURE-----
|
||||||
|
|
||||||
|
-----END JESS SIGNATURE-----
|
||||||
|
-----BEGIN JESS SIGNATURE-----
|
||||||
|
Q6VnVmVyc2lvbgFnU3VpdGVJRGdzaWduX3YxZU5vbmNlRA40a/BkRGF0YVhqTYOr
|
||||||
|
TGFiZWxlZEhhc2jEIhkgAXGM7DXNPXlt0AAg4L/stHOtI0V9Bjt17/KcD/ouWKmo
|
||||||
|
U2lnbmVkQXTW/2LH/ueoTWV0YURhdGGComlk
|
||||||
|
rXJlc291cmNlL3BhdGindmVyc2lvbqUwLjAuMWpTaWduYXR1cmVzgaNmU2NoZW1lZ0VkMjU1MTliSURwZmlsZXNpZy10
|
||||||
|
ZXN0LWtleWVWYWx1ZVhA4b1kfIJF7do6OcJnemQ5mtj/ZyMFJWWTmD1W5KvkpZac
|
||||||
|
2AP5f+dDJhzWBHsoSXTCl6uA3DA3+RbABMYAZn6eDg
|
||||||
|
-----END JESS SIGNATURE-----
|
||||||
|
end`)
|
||||||
|
|
||||||
|
testFileSigFormat2 = []byte(`test data 1
|
||||||
|
-----BEGIN JESS SIGNATURE-----
|
||||||
|
invalid sig
|
||||||
|
-----END JESS SIGNATURE-----
|
||||||
|
test data 2`)
|
||||||
|
|
||||||
|
testFileSigFormat3 = []byte(`test data 1
|
||||||
|
-----BEGIN JESS SIGNATURE-----
|
||||||
|
invalid sig
|
||||||
|
-----END JESS SIGNATURE-----
|
||||||
|
test data 2
|
||||||
|
-----BEGIN JESS SIGNATURE-----
|
||||||
|
Q6VnVmVyc2lvbgFnU3VpdGVJRGdzaWduX3YxZU5vbmNlRA40a/BkRGF0YVhqTYOr
|
||||||
|
TGFiZWxlZEhhc2jEIhkgAXGM7DXNPXlt0AAg4L/stHOtI0V9Bjt17/KcD/ouWKmo
|
||||||
|
U2lnbmVkQXTW/2LH/ueoTWV0YURhdGGComlkrXJlc291cmNlL3BhdGindmVyc2lv
|
||||||
|
bqUwLjAuMWpTaWduYXR1cmVzgaNmU2NoZW1lZ0VkMjU1MTliSURwZmlsZXNpZy10
|
||||||
|
ZXN0LWtleWVWYWx1ZVhA4b1kfIJF7do6OcJnemQ5mtj/ZyMFJWWTmD1W5KvkpZac
|
||||||
|
2AP5f+dDJhzWBHsoSXTCl6uA3DA3+RbABMYAZn6eDg
|
||||||
|
-----END JESS SIGNATURE-----`)
|
||||||
|
|
||||||
|
testFileSigFormat4 = []byte(`test data 1
|
||||||
|
test data 2
|
||||||
|
-----BEGIN JESS SIGNATURE-----
|
||||||
|
Q6VnVmVyc2lvbgFnU3VpdGVJRGdzaWduX3YxZU5vbmNlRA40a/BkRGF0YVhqTYOr
|
||||||
|
TGFiZWxlZEhhc2jEIhkgAXGM7DXNPXlt0AAg4L/stHOtI0V9Bjt17/KcD/ouWKmo
|
||||||
|
U2lnbmVkQXTW/2LH/ueoTWV0YURhdGGComlkrXJlc291cmNlL3BhdGindmVyc2lv
|
||||||
|
bqUwLjAuMWpTaWduYXR1cmVzgaNmU2NoZW1lZ0VkMjU1MTliSURwZmlsZXNpZy10
|
||||||
|
ZXN0LWtleWVWYWx1ZVhA4b1kfIJF7do6OcJnemQ5mtj/ZyMFJWWTmD1W5KvkpZac
|
||||||
|
2AP5f+dDJhzWBHsoSXTCl6uA3DA3+RbABMYAZn6eDg
|
||||||
|
-----END JESS SIGNATURE-----`)
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileSigFormatParsing(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
sigs, err := ParseSigFile(testFileSigFormat1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(sigs) != 2 {
|
||||||
|
t.Fatalf("expected two signatures, got %d", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
newFile, err := AddToSigFile(sigs[0], testFileSigFormat2, false)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(newFile, testFileSigFormat3) {
|
||||||
|
t.Fatalf("unexpected output:\n%s", string(newFile))
|
||||||
|
}
|
||||||
|
newFile, err = AddToSigFile(sigs[0], testFileSigFormat2, true)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(newFile, testFileSigFormat4) {
|
||||||
|
t.Fatalf("unexpected output:\n%s", string(newFile))
|
||||||
|
}
|
||||||
|
}
|
119
filesig/helpers.go
Normal file
119
filesig/helpers.go
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
package jess
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/safing/jess"
|
||||||
|
"github.com/safing/jess/hashtools"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SignFile signs a file and replaces the signature file with a new one.
|
||||||
|
func SignFile(dataFilePath, signatureFilePath string, metaData map[string]string, envelope *jess.Envelope, trustStore jess.TrustStore) (fileData *FileData, err error) {
|
||||||
|
// Load encryption suite.
|
||||||
|
if err := envelope.LoadSuite(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the used hashing algorithm from the suite.
|
||||||
|
var hashTool *hashtools.HashTool
|
||||||
|
for _, toolID := range envelope.Suite().Tools {
|
||||||
|
if strings.Contains(toolID, "(") {
|
||||||
|
hashToolID := strings.Trim(strings.Split(toolID, "(")[0], "()")
|
||||||
|
hashTool, _ = hashtools.Get(hashToolID)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hashTool == nil {
|
||||||
|
return nil, errors.New("suite not suitable for file signing")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the data file.
|
||||||
|
fileHash, err := hashTool.LabeledHasher().DigestFile(dataFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to hash file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign the file data.
|
||||||
|
signature, fileData, err := SignFileData(fileHash, metaData, envelope, trustStore)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to sign file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make signature section for saving to disk.
|
||||||
|
signatureSection, err := MakeSigFileSection(signature)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to format signature for file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the signature to file.
|
||||||
|
if err := ioutil.WriteFile(signatureFilePath, signatureSection, 0o0644); err != nil { //nolint:gosec
|
||||||
|
return nil, fmt.Errorf("failed to write signature to file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileData, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyFile verifies the given files and returns the verified file data.
|
||||||
|
// If an error is returned, there was an error in at least some part of the process.
|
||||||
|
// Any returned file data struct must be checked for an verification error.
|
||||||
|
func VerifyFile(dataFilePath, signatureFilePath string, metaData map[string]string, trustStore jess.TrustStore) (verifiedFileData []*FileData, err error) {
|
||||||
|
var lastErr error
|
||||||
|
|
||||||
|
// Read signature from file.
|
||||||
|
sigFileData, err := ioutil.ReadFile(signatureFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read signature file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract all signatures.
|
||||||
|
sigs, err := ParseSigFile(sigFileData)
|
||||||
|
switch {
|
||||||
|
case len(sigs) == 0 && err != nil:
|
||||||
|
return nil, fmt.Errorf("failed to parse signature file: %w", err)
|
||||||
|
case len(sigs) == 0:
|
||||||
|
return nil, errors.New("no signatures found in signature file")
|
||||||
|
case err != nil:
|
||||||
|
lastErr = fmt.Errorf("failed to parse signature file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify all signatures.
|
||||||
|
goodFileData := make([]*FileData, 0, len(sigs))
|
||||||
|
var badFileData []*FileData
|
||||||
|
for _, sigLetter := range sigs {
|
||||||
|
// Verify signature.
|
||||||
|
fileData, err := VerifyFileData(sigLetter, metaData, trustStore)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
if fileData != nil {
|
||||||
|
fileData.verificationError = err
|
||||||
|
badFileData = append(badFileData, fileData)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the file.
|
||||||
|
fileHash, err := fileData.FileHash().Algorithm().DigestFile(dataFilePath)
|
||||||
|
if err != nil {
|
||||||
|
lastErr = err
|
||||||
|
fileData.verificationError = err
|
||||||
|
badFileData = append(badFileData, fileData)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the hash matches.
|
||||||
|
if !fileData.FileHash().Equal(fileHash) {
|
||||||
|
lastErr = errors.New("signature invalid: file was modified")
|
||||||
|
fileData.verificationError = lastErr
|
||||||
|
badFileData = append(badFileData, fileData)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add verified file data to list for return.
|
||||||
|
goodFileData = append(goodFileData, fileData)
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(goodFileData, badFileData...), lastErr
|
||||||
|
}
|
112
filesig/main.go
Normal file
112
filesig/main.go
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
package jess
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/safing/jess"
|
||||||
|
"github.com/safing/jess/lhash"
|
||||||
|
"github.com/safing/portbase/formats/dsd"
|
||||||
|
)
|
||||||
|
|
||||||
|
var fileSigRequirements = jess.NewRequirements().
|
||||||
|
Remove(jess.RecipientAuthentication).
|
||||||
|
Remove(jess.Confidentiality)
|
||||||
|
|
||||||
|
// FileData describes a file that is signed.
|
||||||
|
type FileData struct {
|
||||||
|
LabeledHash []byte
|
||||||
|
fileHash *lhash.LabeledHash
|
||||||
|
|
||||||
|
SignedAt time.Time
|
||||||
|
MetaData map[string]string
|
||||||
|
|
||||||
|
verificationError error
|
||||||
|
}
|
||||||
|
|
||||||
|
// FileHash returns the labeled hash of the file that was signed.
|
||||||
|
func (fd *FileData) FileHash() *lhash.LabeledHash {
|
||||||
|
return fd.fileHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerificationError returns the error encountered during verification.
|
||||||
|
func (fd *FileData) VerificationError() error {
|
||||||
|
return fd.verificationError
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignFileData signs the given file checksum and metadata.
|
||||||
|
func SignFileData(fileHash *lhash.LabeledHash, metaData map[string]string, envelope *jess.Envelope, trustStore jess.TrustStore) (letter *jess.Letter, fd *FileData, err error) {
|
||||||
|
// Create session.
|
||||||
|
session, err := envelope.Correspondence(trustStore)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create struct and transform data into serializable format to be signed.
|
||||||
|
fd = &FileData{
|
||||||
|
SignedAt: time.Now().Truncate(time.Second),
|
||||||
|
fileHash: fileHash,
|
||||||
|
MetaData: metaData,
|
||||||
|
}
|
||||||
|
fd.LabeledHash = fd.fileHash.Bytes()
|
||||||
|
|
||||||
|
// Serialize file signature.
|
||||||
|
fileData, err := dsd.Dump(fd, dsd.MsgPack)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to serialize file signature data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sign data.
|
||||||
|
letter, err = session.Close(fileData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("failed to sign: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return letter, fd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// VerifyFileData verifies the given signed file data and returns the file data.
|
||||||
|
// If an error is returned, there was an error in at least some part of the process.
|
||||||
|
// Any returned file data struct must be checked for an verification error.
|
||||||
|
func VerifyFileData(letter *jess.Letter, requiredMetaData map[string]string, trustStore jess.TrustStore) (fd *FileData, err error) {
|
||||||
|
// Parse data.
|
||||||
|
fd = &FileData{}
|
||||||
|
_, err = dsd.Load(letter.Data, fd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse file signature data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify signature and get data.
|
||||||
|
_, err = letter.Open(fileSigRequirements, trustStore)
|
||||||
|
if err != nil {
|
||||||
|
fd.verificationError = fmt.Errorf("failed to verify file signature: %w", err)
|
||||||
|
return fd, fd.verificationError
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the required metadata matches.
|
||||||
|
for reqKey, reqValue := range requiredMetaData {
|
||||||
|
sigMetaValue, ok := fd.MetaData[reqKey]
|
||||||
|
if !ok {
|
||||||
|
fd.verificationError = fmt.Errorf("missing required metadata key %q", reqKey)
|
||||||
|
return fd, fd.verificationError
|
||||||
|
}
|
||||||
|
if sigMetaValue != reqValue {
|
||||||
|
fd.verificationError = fmt.Errorf("required metadata %q=%q does not match the file's value %q", reqKey, reqValue, sigMetaValue)
|
||||||
|
return fd, fd.verificationError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse labeled hash.
|
||||||
|
fd.fileHash, err = lhash.Load(fd.LabeledHash)
|
||||||
|
if err != nil {
|
||||||
|
fd.verificationError = fmt.Errorf("failed to parse file checksum: %w", err)
|
||||||
|
return fd, fd.verificationError
|
||||||
|
}
|
||||||
|
|
||||||
|
return fd, nil
|
||||||
|
}
|
130
filesig/main_test.go
Normal file
130
filesig/main_test.go
Normal file
|
@ -0,0 +1,130 @@
|
||||||
|
package jess
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/safing/jess"
|
||||||
|
"github.com/safing/jess/lhash"
|
||||||
|
"github.com/safing/jess/tools"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
testTrustStore = jess.NewMemTrustStore()
|
||||||
|
testData1 = "The quick brown fox jumps over the lazy dog. "
|
||||||
|
|
||||||
|
testFileSigMetaData1 = map[string]string{
|
||||||
|
"key1": "value1",
|
||||||
|
"key2": "value2",
|
||||||
|
}
|
||||||
|
testFileSigMetaData1x = map[string]string{
|
||||||
|
"key1": "value1x",
|
||||||
|
}
|
||||||
|
testFileSigMetaData2 = map[string]string{
|
||||||
|
"key3": "value3",
|
||||||
|
"key4": "value4",
|
||||||
|
}
|
||||||
|
testFileSigMetaData3 = map[string]string{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFileSigs(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testFileSigningWithOptions(t, testFileSigMetaData1, testFileSigMetaData1, true)
|
||||||
|
testFileSigningWithOptions(t, testFileSigMetaData1, testFileSigMetaData1x, false)
|
||||||
|
testFileSigningWithOptions(t, testFileSigMetaData2, testFileSigMetaData2, true)
|
||||||
|
testFileSigningWithOptions(t, testFileSigMetaData1, testFileSigMetaData2, false)
|
||||||
|
testFileSigningWithOptions(t, testFileSigMetaData2, testFileSigMetaData1, false)
|
||||||
|
testFileSigningWithOptions(t, testFileSigMetaData1, testFileSigMetaData3, true)
|
||||||
|
testFileSigningWithOptions(t, testFileSigMetaData3, testFileSigMetaData1, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testFileSigningWithOptions(t *testing.T, signingMetaData, verifyingMetaData map[string]string, shouldSucceed bool) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
// 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-filesig-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash "file".
|
||||||
|
fileHash := lhash.BLAKE2b_256.Digest([]byte(testData1))
|
||||||
|
|
||||||
|
// Make envelope.
|
||||||
|
envelope := jess.NewUnconfiguredEnvelope()
|
||||||
|
envelope.SuiteID = jess.SuiteSignV1
|
||||||
|
envelope.Senders = []*jess.Signet{s}
|
||||||
|
|
||||||
|
// Sign data.
|
||||||
|
letter, fileData, err := SignFileData(fileHash, signingMetaData, envelope, testTrustStore)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the checksum made it.
|
||||||
|
if len(fileData.LabeledHash) == 0 {
|
||||||
|
t.Fatal("missing labeled hash")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify signature.
|
||||||
|
_, err = VerifyFileData(letter, verifyingMetaData, testTrustStore)
|
||||||
|
if (err == nil) != shouldSucceed {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getOrMakeSignet(t *testing.T, tool tools.ToolLogic, recipient bool, signetID string) (*jess.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 := jess.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
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue