-
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 filesig package for signing files in a common way
- Loading branch information
Showing
5 changed files
with
681 additions
and
0 deletions.
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,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 | ||
} |
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 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)) | ||
} | ||
} |
Oops, something went wrong.