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