Skip to content

Commit 0da993a

Browse files
Mzack9999dogancanbakirGuyGoldenberg
authored
Merge commit from fork
* fix template signature verification * fix signature pattern check * add tests * remove signature count constraint, check for lines len * Add more tests * Centralize signature extraction logic in signer package * Move signature handling in Sign function to beginning * Remove comment * Revert `NewTemplateSigVerifier` * update tests * use ExtractSignatureAndContent func * Allow signing code templates * Remove unused const --------- Co-authored-by: Doğan Can Bakır <[email protected]> Co-authored-by: Guy Goldenberg <[email protected]>
1 parent 4d5eb9c commit 0da993a

File tree

3 files changed

+161
-25
lines changed

3 files changed

+161
-25
lines changed

pkg/templates/signer/tmpl_signer.go

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import (
1111
"errors"
1212
"fmt"
1313
"os"
14-
"regexp"
1514
"strings"
1615
"sync"
1716

@@ -21,18 +20,21 @@ import (
2120
)
2221

2322
var (
24-
ReDigest = regexp.MustCompile(`(?m)^#\sdigest:\s.+$`)
2523
ErrUnknownAlgorithm = errors.New("unknown algorithm")
2624
SignaturePattern = "# digest: "
2725
SignatureFmt = SignaturePattern + "%x" + ":%v" // `#digest: <signature>:<fragment>`
2826
)
2927

30-
func RemoveSignatureFromData(data []byte) []byte {
31-
return bytes.Trim(ReDigest.ReplaceAll(data, []byte("")), "\n")
32-
}
33-
34-
func GetSignatureFromData(data []byte) []byte {
35-
return ReDigest.Find(data)
28+
// ExtractSignatureAndContent extracts the signature (if present) and returns the content without the signature
29+
func ExtractSignatureAndContent(data []byte) (signature, content []byte) {
30+
dataStr := string(data)
31+
if idx := strings.LastIndex(dataStr, SignaturePattern); idx != -1 {
32+
signature = []byte(strings.TrimSpace(dataStr[idx:]))
33+
content = []byte(strings.TrimSpace(dataStr[:idx]))
34+
} else {
35+
content = data
36+
}
37+
return
3638
}
3739

3840
// SignableTemplate is a template that can be signed
@@ -69,26 +71,29 @@ func (t *TemplateSigner) GetUserFragment() string {
6971

7072
// Sign signs the given template with the template signer and returns the signature
7173
func (t *TemplateSigner) Sign(data []byte, tmpl SignableTemplate) (string, error) {
74+
existingSignature, content := ExtractSignatureAndContent(data)
75+
7276
// while re-signing template check if it has a code protocol
7377
// if it does then verify that it is signed by current signer
7478
// if not then return error
7579
if tmpl.HasCodeProtocol() {
76-
sig := GetSignatureFromData(data)
77-
arr := strings.SplitN(string(sig), ":", 3)
78-
if len(arr) == 2 {
79-
// signature has no fragment
80-
return "", errorutil.NewWithTag("signer", "re-signing code templates are not allowed for security reasons.")
81-
}
82-
if len(arr) == 3 {
83-
// signature has fragment verify if it is equal to current fragment
84-
fragment := t.GetUserFragment()
85-
if fragment != arr[2] {
80+
if len(existingSignature) > 0 {
81+
arr := strings.SplitN(string(existingSignature), ":", 3)
82+
if len(arr) == 2 {
83+
// signature has no fragment
8684
return "", errorutil.NewWithTag("signer", "re-signing code templates are not allowed for security reasons.")
8785
}
86+
if len(arr) == 3 {
87+
// signature has fragment verify if it is equal to current fragment
88+
fragment := t.GetUserFragment()
89+
if fragment != arr[2] {
90+
return "", errorutil.NewWithTag("signer", "re-signing code templates are not allowed for security reasons.")
91+
}
92+
}
8893
}
8994
}
9095

91-
buff := bytes.NewBuffer(RemoveSignatureFromData(data))
96+
buff := bytes.NewBuffer(content)
9297
// if file has any imports process them
9398
for _, file := range tmpl.GetFileImports() {
9499
bin, err := os.ReadFile(file)
@@ -123,20 +128,24 @@ func (t *TemplateSigner) sign(data []byte) (string, error) {
123128

124129
// Verify verifies the given template with the template signer
125130
func (t *TemplateSigner) Verify(data []byte, tmpl SignableTemplate) (bool, error) {
126-
digestData := ReDigest.Find(data)
127-
if len(digestData) == 0 {
128-
return false, errors.New("digest not found")
131+
signature, content := ExtractSignatureAndContent(data)
132+
if len(signature) == 0 {
133+
return false, errors.New("no signature found")
134+
}
135+
136+
if !bytes.HasPrefix(signature, []byte(SignaturePattern)) {
137+
return false, errors.New("signature must be at the end of the template")
129138
}
130139

131-
digestData = bytes.TrimSpace(bytes.TrimPrefix(digestData, []byte(SignaturePattern)))
140+
digestData := bytes.TrimSpace(bytes.TrimPrefix(signature, []byte(SignaturePattern)))
132141
// remove fragment from digest as it is used for re-signing purposes only
133142
digestString := strings.TrimSuffix(string(digestData), ":"+t.GetUserFragment())
134143
digest, err := hex.DecodeString(digestString)
135144
if err != nil {
136145
return false, err
137146
}
138147

139-
buff := bytes.NewBuffer(RemoveSignatureFromData(data))
148+
buff := bytes.NewBuffer(content)
140149
// if file has any imports process them
141150
for _, file := range tmpl.GetFileImports() {
142151
bin, err := os.ReadFile(file)
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package signer
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
const (
14+
testCertFile = "../../../integration_tests/protocols/keys/ci.crt"
15+
testKeyFile = "../../../integration_tests/protocols/keys/ci-private-key.pem"
16+
)
17+
18+
type mockSignableTemplate struct {
19+
imports []string
20+
hasCode bool
21+
}
22+
23+
func (m *mockSignableTemplate) GetFileImports() []string {
24+
return m.imports
25+
}
26+
27+
func (m *mockSignableTemplate) HasCodeProtocol() bool {
28+
return m.hasCode
29+
}
30+
31+
var signer, _ = NewTemplateSignerFromFiles(testCertFile, testKeyFile)
32+
33+
func TestTemplateSignerSignAndVerify(t *testing.T) {
34+
tempDir := t.TempDir()
35+
36+
tests := []struct {
37+
name string
38+
data []byte
39+
tmpl SignableTemplate
40+
wantSignErr bool
41+
wantVerifyErr bool
42+
wantVerified bool
43+
modifyAfterSign func([]byte) []byte
44+
}{
45+
{
46+
name: "Simple template",
47+
data: []byte("id: test-template\ninfo:\n name: Test Template"),
48+
tmpl: &mockSignableTemplate{},
49+
wantVerified: true,
50+
},
51+
{
52+
name: "Template with imports",
53+
data: []byte("id: test-template\ninfo:\n name: Test Template"),
54+
tmpl: &mockSignableTemplate{imports: []string{
55+
filepath.Join(tempDir, "import1.yaml"),
56+
filepath.Join(tempDir, "import2.yaml"),
57+
}},
58+
wantVerified: true,
59+
},
60+
{
61+
name: "Template with code protocol",
62+
data: []byte("id: test-template\ninfo:\n name: Test Template\n\ncode:\n - engine: bash\n source: echo 'Hello, World!'"),
63+
tmpl: &mockSignableTemplate{hasCode: true},
64+
wantSignErr: false,
65+
wantVerified: true,
66+
},
67+
{
68+
name: "Tampered template",
69+
data: []byte("id: test-template\ninfo:\n name: Test Template"),
70+
tmpl: &mockSignableTemplate{},
71+
modifyAfterSign: func(data []byte) []byte {
72+
signatureIndex := bytes.LastIndex(data, []byte(SignaturePattern))
73+
if signatureIndex == -1 {
74+
return data
75+
}
76+
return append(data[:signatureIndex], append([]byte("# Tampered content\n"), data[signatureIndex:]...)...)
77+
},
78+
wantVerified: false,
79+
},
80+
{
81+
name: "Invalid signature",
82+
data: []byte("id: test-template\ninfo:\n name: Test Template"),
83+
tmpl: &mockSignableTemplate{},
84+
modifyAfterSign: func(data []byte) []byte {
85+
return append(bytes.TrimSuffix(data, []byte("\n")), []byte("\n# digest: invalid_signature:fragment")...)
86+
},
87+
wantVerifyErr: true,
88+
wantVerified: false,
89+
},
90+
}
91+
92+
for _, tt := range tests {
93+
t.Run(tt.name, func(t *testing.T) {
94+
// Create import files if needed
95+
for _, imp := range tt.tmpl.GetFileImports() {
96+
err := os.WriteFile(imp, []byte("imported content"), 0644)
97+
require.NoError(t, err, "Failed to create import file")
98+
}
99+
100+
// Sign the template
101+
signature, err := signer.Sign(tt.data, tt.tmpl)
102+
if tt.wantSignErr {
103+
assert.Error(t, err, "Expected an error during signing")
104+
return
105+
}
106+
require.NoError(t, err, "Failed to sign template")
107+
108+
// Append signature to the template data
109+
signedData := append(tt.data, []byte("\n"+signature)...)
110+
111+
// Apply any modifications after signing if specified
112+
if tt.modifyAfterSign != nil {
113+
signedData = tt.modifyAfterSign(signedData)
114+
}
115+
116+
// Verify the signature
117+
verified, err := signer.Verify(signedData, tt.tmpl)
118+
if tt.wantVerifyErr {
119+
assert.Error(t, err, "Expected an error during verification")
120+
} else {
121+
assert.NoError(t, err, "Unexpected error during verification")
122+
}
123+
assert.Equal(t, tt.wantVerified, verified, "Unexpected verification result")
124+
})
125+
}
126+
}

pkg/templates/template_sign.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,12 @@ func SignTemplate(templateSigner *signer.TemplateSigner, templatePath string) er
7575
return ErrNotATemplate
7676
}
7777
if !template.Verified {
78+
_, content := signer.ExtractSignatureAndContent(bin)
7879
signatureData, err := templateSigner.Sign(bin, template)
7980
if err != nil {
8081
return err
8182
}
82-
buff := bytes.NewBuffer(signer.RemoveSignatureFromData(bin))
83+
buff := bytes.NewBuffer(content)
8384
buff.WriteString("\n" + signatureData)
8485
return os.WriteFile(templatePath, buff.Bytes(), 0644)
8586
}

0 commit comments

Comments
 (0)