Skip to content

Conversation

@dwisiswant0
Copy link
Member

@dwisiswant0 dwisiswant0 commented Jan 21, 2026

Proposed changes

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).

Top 10 deltas:

$ go tool pprof -top -nodecount=10 -base 3.6.2.cpu patch.cpu
File: nuclei
Build ID: c7042922d4c9a1b750e6e1e27ffe9582b44ddeee
Type: cpu
Time: 2026-01-21 14:31:10 WIB
Duration: 18.24s, Total samples = 34.78s (190.68%)
Showing nodes accounting for -1.75s, 5.03% of 34.78s total
Dropped 624 nodes (cum <= 0.17s)
Showing top 10 nodes out of 668
      flat  flat%   sum%        cum   cum%
    -0.43s  1.24%  1.24%     -0.43s  1.24%  p256MulInternal
    -0.37s  1.06%  2.30%     -0.37s  1.06%  p256SqrInternal
    -0.30s  0.86%  3.16%     -0.49s  1.41%  runtime.pcvalue
    -0.15s  0.43%  3.59%     -0.15s  0.43%  crypto/internal/fips140/nistec.p256OrdSqr
    -0.15s  0.43%  4.03%     -0.61s  1.75%  crypto/internal/fips140/nistec.p256PointDoubleAsm
    -0.14s   0.4%  4.43%     -0.14s   0.4%  gopkg.in/yaml%2ev2.is_blankz
    -0.12s  0.35%  4.77%     -0.09s  0.26%  runtime.step
    -0.10s  0.29%  5.06%     -0.24s  0.69%  gopkg.in/yaml%2ev2.(*decoder).unmarshal
     0.10s  0.29%  4.77%      0.07s   0.2%  gopkg.in/yaml%2ev2.yaml_parser_scan_flow_scalar
    -0.09s  0.26%  5.03%     -0.09s  0.26%  internal/runtime/syscall.Syscall6

Checklist

  • Pull request is created against the dev branch
  • All checks passed (lint, unit/integration/regression tests etc.) with my changes
  • I have added tests that prove my fix is effective or that my feature works
  • I have added necessary documentation (if appropriate)

Summary by CodeRabbit

  • Performance Improvements
    • Enhanced template verification efficiency by implementing caching mechanisms to reduce redundant verification checks during template compilation and execution, resulting in faster template processing.

✏️ Tip: You can customize this high-level summary in your review settings.

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]>
@auto-assign auto-assign bot requested a review from Mzack9999 January 21, 2026 08:15
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

Walkthrough

A template verification caching mechanism is introduced across the codebase. A new TemplateVerification struct stores cached verification data, a callback field is added to ExecutorOptions, the loader wires this callback to retrieve cached verification from stored metadata, and template compilation checks for cached verification before performing verification operations.

Changes

Cohort / File(s) Summary
Verification Infrastructure
pkg/protocols/protocols.go
Added TemplateVerification struct to hold cached verification data (Verified, Verifier). Introduced TemplateVerificationCallback field to ExecutorOptions for retrieving cached verification. Updated Copy() method to propagate the new callback field.
Loader Integration
pkg/catalog/loader/loader.go
Implemented getTemplateVerification(templatePath string) helper method on Store to retrieve cached TemplateVerification from metadata index. Wired the callback in New() when ExecutorOptions is provided, enabling verification data retrieval during execution.
Compilation Optimization
pkg/templates/compile.go
Added pre-check in parseTemplate for cached verification via callback. If cached verification exists, populates template verification fields and sets RawTemplate conditionally based on verifier type, with early return. Preserves existing verification loop for non-cached paths.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Hop hop, cache it quick!
Verification's now a trick,
No re-checking, templates fly,
Faster parsing way up high!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'perf: cache template signature verification' directly and clearly describes the main performance optimization introduced in the changeset—adding caching for template signature verification to reduce redundant ECDSA checks.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@dwisiswant0
Copy link
Member Author

@coderabbitai create an issue for this. Prefixed with [PERF].

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 21, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

@dwisiswant0, I'll create an issue for this performance optimization.


🏁 Script executed:

#!/bin/bash

# Create a GitHub issue for the performance optimization work
gh issue create \
  --title "[PERF] Cache template signature verification" \
  --body "## Context

This issue tracks the performance optimization work to cache template signature verification results and avoid redundant ECDSA checks.

## Background

Related PR: https://github.com/projectdiscovery/nuclei/pull/6779

The current implementation performs ECDSA verification on every template load, which is expensive and contributes significantly to startup time. Profiling data shows:
- \`TemplateSigner.Verify\` takes ~1.75s
- \`crypto/ecdsa.VerifyASN1\` takes ~1.50s

## Proposed Solution

Add caching mechanism to reuse verified signature data:
- Introduce \`protocols.TemplateVerification\` and callback mechanism to \`protocols.ExecutorOptions\`
- Enable reusing cached verification data from the metadata index
- Update internal \`templates.parseTemplate\` to skip ECDSA verification when cached data is available

This eliminates both \`TemplateSigner.Verify\` and \`crypto/ecdsa.VerifyASN1\` from the hot path, reducing startup time.

## Requested by

`@dwisiswant0`" \
  --assignee dwisiswant0

Length of output: 54


@dwisiswant0, I've created the issue for this performance optimization: #6780

The issue summarizes the caching work for template signature verification and includes a reference to this PR.

@dwisiswant0
Copy link
Member Author

Context: since we already introduced persistent metadata cache (#6630), though there's room to take advantage of it further for perf gains.

@dwisiswant0 dwisiswant0 linked an issue Jan 21, 2026 that may be closed by this pull request
@Mzack9999 Mzack9999 added the Type: Enhancement Most issues will probably ask for additions or changes. label Jan 21, 2026
@Mzack9999 Mzack9999 merged commit 39f67ed into dev Jan 21, 2026
30 of 31 checks passed
@Mzack9999 Mzack9999 deleted the dwisiswant0/perf/cache-template-signature-verification branch January 21, 2026 12:09
@dwisiswant0 dwisiswant0 added this to the v3.7.0 milestone Jan 21, 2026
@dwisiswant0 dwisiswant0 added Type: Optimization Increasing the performance/optimization. Not an issue, just something to consider. and removed Type: Enhancement Most issues will probably ask for additions or changes. labels Jan 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Type: Optimization Increasing the performance/optimization. Not an issue, just something to consider.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[PERF] Cache template signature verification

3 participants