Skip to content

Commit 4534e9c

Browse files
committed
perf: cache template signature verification
to avoid redundant ECDSA checks. Add `protocols.TemplateVerification` & callback mechanism to `protocols.ExecutorOptions` to enable reusing cached verification data from the metadata index. Also updating internal `templates.parseTemplate` func to skip ECDSA verification when cached data is any, and wire the callback in `loader.New` for metadata-backed lookups. Proof: ``` $ go tool pprof -list "signer\..*" -base 3.6.2.cpu patch.cpu Total: 34.78s ROUTINE ======================== github.com/projectdiscovery/nuclei/v3/pkg/templates/signer.(*TemplateSigner).Verify in /home/dw1/Development/PD/nuclei/pkg/templates/signer/tmpl_signer.go 0 -1.75s (flat, cum) 5.03% of Total . . 131:func (t *TemplateSigner) Verify(data []byte, tmpl SignableTemplate) (bool, error) { . -70ms 132: signature, content := ExtractSignatureAndContent(data) . . 133: if len(signature) == 0 { . . 134: return false, errors.New("no signature found") . . 135: } . . 136: . . 137: if !bytes.HasPrefix(signature, []byte(SignaturePattern)) { . . 138: return false, errors.New("signature must be at the end of the template") . . 139: } . . 140: . . 141: digestData := bytes.TrimSpace(bytes.TrimPrefix(signature, []byte(SignaturePattern))) . . 142: // remove fragment from digest as it is used for re-signing purposes only . . 143: digestString := strings.TrimSuffix(string(digestData), ":"+t.GetUserFragment()) . -20ms 144: digest, err := hex.DecodeString(digestString) . . 145: if err != nil { . . 146: return false, err . . 147: } . . 148: . . 149: // normalize content by removing \r\n everywhere since this only done for verification . . 150: // it does not affect the actual template . -40ms 151: content = bytes.ReplaceAll(content, []byte("\r\n"), []byte("\n")) . . 152: . . 153: buff := bytes.NewBuffer(content) . . 154: // if file has any imports process them . . 155: for _, file := range tmpl.GetFileImports() { . . 156: bin, err := os.ReadFile(file) . . 157: if err != nil { . . 158: return false, err . . 159: } . . 160: buff.WriteRune('\n') . . 161: buff.Write(bin) . . 162: } . . 163: . -1.62s 164: return t.verify(buff.Bytes(), digest) . . 165:} . . 166: . . 167:// Verify verifies the given data with the template signer . . 168:// Note: this should not be used for verifying templates as file references . . 169:// in templates are not processed ROUTINE ======================== github.com/projectdiscovery/nuclei/v3/pkg/templates/signer.(*TemplateSigner).verify in /home/dw1/Development/PD/nuclei/pkg/templates/signer/tmpl_signer.go 0 -1.62s (flat, cum) 4.66% of Total . . 170:func (t *TemplateSigner) verify(data, signatureData []byte) (bool, error) { . -50ms 171: dataHash := sha256.Sum256(data) . . 172: . . 173: var signature []byte . -70ms 174: if err := gob.NewDecoder(bytes.NewReader(signatureData)).Decode(&signature); err != nil { . . 175: return false, err . . 176: } . -1.50s 177: return ecdsa.VerifyASN1(t.handler.ecdsaPubKey, dataHash[:], signature), nil . . 178:} . . 179: . . 180:// NewTemplateSigner creates a new signer for signing templates . . 181:func NewTemplateSigner(cert, privateKey []byte) (*TemplateSigner, error) { . . 182: handler := &KeyHandler{} ROUTINE ======================== github.com/projectdiscovery/nuclei/v3/pkg/templates/signer.ExtractSignatureAndContent in /home/dw1/Development/PD/nuclei/pkg/templates/signer/tmpl_signer.go 0 -70ms (flat, cum) 0.2% of Total . . 29:func ExtractSignatureAndContent(data []byte) (signature, content []byte) { . -50ms 30: dataStr := string(data) . -20ms 31: if idx := strings.LastIndex(dataStr, SignaturePattern); idx != -1 { . . 32: signature = []byte(strings.TrimSpace(dataStr[idx:])) . . 33: content = bytes.TrimSpace(data[:idx]) . . 34: } else { . . 35: content = data . . 36: } $ go tool pprof -list "crypto/ecdsa\.VerifyASN1" 3.6.2.cpu patch.cpu Total: 34.80s ROUTINE ======================== crypto/ecdsa.VerifyASN1 in /usr/local/go/src/crypto/ecdsa/ecdsa.go 0 1.50s (flat, cum) 4.31% of Total . . 500:func VerifyASN1(pub *PublicKey, hash, sig []byte) bool { . . 501: if boring.Enabled { . . 502: key, err := boringPublicKey(pub) . . 503: if err != nil { . . 504: return false . . 505: } . . 506: return boring.VerifyECDSA(key, hash, sig) . . 507: } . . 508: boring.UnreachableExceptTests() . . 509: . . 510: switch pub.Curve.Params() { . . 511: case elliptic.P224().Params(): . . 512: return verifyFIPS(ecdsa.P224(), pub, hash, sig) . . 513: case elliptic.P256().Params(): . 1.50s 514: return verifyFIPS(ecdsa.P256(), pub, hash, sig) . . 515: case elliptic.P384().Params(): . . 516: return verifyFIPS(ecdsa.P384(), pub, hash, sig) . . 517: case elliptic.P521().Params(): . . 518: return verifyFIPS(ecdsa.P521(), pub, hash, sig) . . 519: default: ``` This eliminates `TemplateSigner.Verify` (~1.75s) and `crypto/ecdsa.VerifyASN1` (~1.50s) from the hot path (read: reduces startup time). Signed-off-by: Dwi Siswanto <[email protected]>
1 parent ee8287a commit 4534e9c

File tree

3 files changed

+44
-0
lines changed

3 files changed

+44
-0
lines changed

pkg/catalog/loader/loader.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,9 @@ func New(cfg *Config) (*Store, error) {
172172
// Initialize metadata index and filter (load from disk & cache for reuse)
173173
store.metadataIndex = store.loadTemplatesIndex()
174174
store.indexFilter = store.buildIndexFilter()
175+
if cfg.ExecutorOptions != nil {
176+
cfg.ExecutorOptions.TemplateVerificationCallback = store.getTemplateVerification
177+
}
175178
store.saveMetadataIndexOnce = sync.OnceFunc(func() {
176179
if store.metadataIndex == nil {
177180
return
@@ -246,6 +249,22 @@ func New(cfg *Config) (*Store, error) {
246249
return store, nil
247250
}
248251

252+
func (store *Store) getTemplateVerification(templatePath string) *protocols.TemplateVerification {
253+
if store.metadataIndex == nil {
254+
return nil
255+
}
256+
257+
metadata, found := store.metadataIndex.Get(templatePath)
258+
if !found {
259+
return nil
260+
}
261+
262+
return &protocols.TemplateVerification{
263+
Verified: metadata.Verified,
264+
Verifier: metadata.TemplateVerifier,
265+
}
266+
}
267+
249268
func handleTemplatesEditorURLs(input string) string {
250269
parsed, err := url.Parse(input)
251270
if err != nil {

pkg/protocols/protocols.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@ type Executer interface {
5757
ExecuteWithResults(ctx *scan.ScanContext) ([]*output.ResultEvent, error)
5858
}
5959

60+
// TemplateVerification holds cached verification information for a template.
61+
type TemplateVerification struct {
62+
Verified bool
63+
Verifier string
64+
}
65+
6066
// ExecutorOptions contains the configuration options for executer clients
6167
type ExecutorOptions struct {
6268
// TemplateID is the ID of the template for the request
@@ -67,6 +73,9 @@ type ExecutorOptions struct {
6773
TemplateInfo model.Info
6874
// TemplateVerifier is the verifier for the template
6975
TemplateVerifier string
76+
// TemplateVerificationCallback returns cached verification info for a template path.
77+
// If it returns nil, verification should be computed normally.
78+
TemplateVerificationCallback func(templatePath string) *TemplateVerification
7079
// RawTemplate is the raw template for the request
7180
RawTemplate []byte
7281
// Output is a writer interface for writing output events from executer.
@@ -266,6 +275,7 @@ func (e *ExecutorOptions) Copy() *ExecutorOptions {
266275
TemplatePath: e.TemplatePath,
267276
TemplateInfo: e.TemplateInfo,
268277
TemplateVerifier: e.TemplateVerifier,
278+
TemplateVerificationCallback: e.TemplateVerificationCallback,
269279
RawTemplate: e.RawTemplate,
270280
Output: e.Output,
271281
Options: e.Options,

pkg/templates/compile.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,19 @@ func parseTemplate(data []byte, srcOptions *protocols.ExecutorOptions) (*Templat
580580

581581
// check if the template is verified
582582
// only valid templates can be verified or signed
583+
if options.TemplateVerificationCallback != nil && options.TemplatePath != "" {
584+
if cached := options.TemplateVerificationCallback(options.TemplatePath); cached != nil {
585+
template.Verified = cached.Verified
586+
template.TemplateVerifier = cached.Verifier
587+
options.TemplateVerifier = cached.Verifier
588+
//nolint
589+
if !(template.Verified && template.TemplateVerifier == "projectdiscovery/nuclei-templates") {
590+
template.Options.RawTemplate = data
591+
}
592+
return template, nil
593+
}
594+
}
595+
583596
var verifier *signer.TemplateSigner
584597
for _, verifier = range signer.DefaultTemplateVerifiers {
585598
template.Verified, _ = verifier.Verify(data, template)
@@ -592,10 +605,12 @@ func parseTemplate(data []byte, srcOptions *protocols.ExecutorOptions) (*Templat
592605
}
593606
}
594607
options.TemplateVerifier = template.TemplateVerifier
608+
595609
//nolint
596610
if !(template.Verified && verifier.Identifier() == "projectdiscovery/nuclei-templates") {
597611
template.Options.RawTemplate = data
598612
}
613+
599614
return template, nil
600615
}
601616

0 commit comments

Comments
 (0)