Files
Dwi Siswanto 4534e9cb30 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 <git@dw1.io>
2026-01-21 15:08:47 +07:00
..
2025-10-10 17:32:54 +02:00