Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/templates/signer/handler.go
2070 views
1
package signer
2
3
import (
4
"bytes"
5
"crypto/ecdsa"
6
"crypto/elliptic"
7
"crypto/rand"
8
"crypto/x509"
9
"crypto/x509/pkix"
10
"encoding/pem"
11
"fmt"
12
"math/big"
13
"os"
14
"path/filepath"
15
"time"
16
17
"github.com/projectdiscovery/gologger"
18
fileutil "github.com/projectdiscovery/utils/file"
19
"github.com/rs/xid"
20
"golang.org/x/term"
21
)
22
23
const (
24
CertType = "PD NUCLEI USER CERTIFICATE"
25
PrivateKeyType = "PD NUCLEI USER PRIVATE KEY"
26
CertFilename = "nuclei-user.crt"
27
PrivateKeyFilename = "nuclei-user-private-key.pem"
28
CertEnvVarName = "NUCLEI_USER_CERTIFICATE"
29
PrivateKeyEnvName = "NUCLEI_USER_PRIVATE_KEY"
30
)
31
32
var (
33
ErrNoCertificate = fmt.Errorf("nuclei user certificate not found")
34
ErrNoPrivateKey = fmt.Errorf("nuclei user private key not found")
35
SkipGeneratingKeys = false
36
noUserPassphrase = false
37
)
38
39
// KeyHandler handles the key generation and management
40
// of signer public and private keys
41
type KeyHandler struct {
42
UserCert []byte
43
PrivateKey []byte
44
cert *x509.Certificate
45
ecdsaPubKey *ecdsa.PublicKey
46
ecdsaKey *ecdsa.PrivateKey
47
}
48
49
// ReadCert reads the user certificate from environment variable or given directory
50
func (k *KeyHandler) ReadCert(envName, dir string) error {
51
// read from env
52
if cert := k.getEnvContent(envName); cert != nil {
53
k.UserCert = cert
54
return nil
55
}
56
// read from disk
57
if cert, err := os.ReadFile(filepath.Join(dir, CertFilename)); err == nil {
58
k.UserCert = cert
59
return nil
60
}
61
return ErrNoCertificate
62
}
63
64
// ReadPrivateKey reads the private key from environment variable or given directory
65
func (k *KeyHandler) ReadPrivateKey(envName, dir string) error {
66
// read from env
67
if privateKey := k.getEnvContent(envName); privateKey != nil {
68
k.PrivateKey = privateKey
69
return nil
70
}
71
// read from disk
72
if privateKey, err := os.ReadFile(filepath.Join(dir, PrivateKeyFilename)); err == nil {
73
k.PrivateKey = privateKey
74
return nil
75
}
76
return ErrNoPrivateKey
77
}
78
79
// ParseUserCert parses the user certificate and returns the public key
80
func (k *KeyHandler) ParseUserCert() error {
81
block, _ := pem.Decode(k.UserCert)
82
if block == nil {
83
return fmt.Errorf("failed to parse PEM block containing the certificate")
84
}
85
cert, err := x509.ParseCertificate(block.Bytes)
86
if err != nil {
87
return err
88
}
89
if cert.Subject.CommonName == "" {
90
return fmt.Errorf("invalid certificate: expected common name to be set")
91
}
92
k.cert = cert
93
var ok bool
94
k.ecdsaPubKey, ok = cert.PublicKey.(*ecdsa.PublicKey)
95
if !ok {
96
return fmt.Errorf("failed to parse ecdsa public key from cert")
97
}
98
return nil
99
}
100
101
// ParsePrivateKey parses the private key and returns the private key
102
func (k *KeyHandler) ParsePrivateKey() error {
103
block, _ := pem.Decode(k.PrivateKey)
104
if block == nil {
105
return fmt.Errorf("failed to parse PEM block containing the private key")
106
}
107
// if pem block is encrypted , decrypt it
108
if x509.IsEncryptedPEMBlock(block) { // nolint: all
109
gologger.Info().Msgf("Private Key is encrypted with passphrase")
110
fmt.Printf("[*] Enter passphrase (exit to abort): ")
111
bin, err := term.ReadPassword(int(os.Stdin.Fd()))
112
if err != nil {
113
return err
114
}
115
fmt.Println()
116
if string(bin) == "exit" {
117
return fmt.Errorf("private key requires passphrase, but none was provided")
118
}
119
block.Bytes, err = x509.DecryptPEMBlock(block, bin) // nolint: all
120
if err != nil {
121
return err
122
}
123
}
124
var err error
125
k.ecdsaKey, err = x509.ParseECPrivateKey(block.Bytes)
126
if err != nil {
127
return err
128
}
129
return nil
130
}
131
132
// GenerateKeyPair generates a new key-pair for signing code templates
133
func (k *KeyHandler) GenerateKeyPair() {
134
135
gologger.Info().Msgf("Generating new key-pair for signing templates")
136
fmt.Printf("[*] Enter User/Organization Name (exit to abort) : ")
137
138
// get user/organization name
139
identifier := ""
140
_, err := fmt.Scanln(&identifier)
141
if err != nil {
142
gologger.Fatal().Msgf("failed to read user/organization name: %s", err)
143
}
144
if identifier == "exit" {
145
gologger.Fatal().Msgf("exiting key-pair generation")
146
}
147
if identifier == "" {
148
gologger.Fatal().Msgf("user/organization name cannot be empty")
149
}
150
151
// generate new key-pair
152
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
153
if err != nil {
154
gologger.Fatal().Msgf("failed to generate ecdsa key-pair: %s", err)
155
}
156
157
// create x509 certificate with user/organization name and public key
158
// self-signed certificate with generated private key
159
k.UserCert, err = k.generateCertWithKey(identifier, privateKey)
160
if err != nil {
161
gologger.Fatal().Msgf("failed to create certificate: %s", err)
162
}
163
164
// marshal private key
165
k.PrivateKey, err = k.marshalPrivateKey(privateKey)
166
if err != nil {
167
gologger.Fatal().Msgf("failed to marshal ecdsa private key: %s", err)
168
}
169
gologger.Info().Msgf("Successfully generated new key-pair for signing templates")
170
}
171
172
// SaveToDisk saves the generated key-pair to the given directory
173
func (k *KeyHandler) SaveToDisk(dir string) error {
174
_ = fileutil.FixMissingDirs(filepath.Join(dir, CertFilename)) // not required but just in case will take care of missing dirs in path
175
if err := os.WriteFile(filepath.Join(dir, CertFilename), k.UserCert, 0600); err != nil {
176
return err
177
}
178
if err := os.WriteFile(filepath.Join(dir, PrivateKeyFilename), k.PrivateKey, 0600); err != nil {
179
return err
180
}
181
return nil
182
}
183
184
// getEnvContent returns the content of the environment variable
185
// if it is a file then it loads its content
186
func (k *KeyHandler) getEnvContent(name string) []byte {
187
val := os.Getenv(name)
188
if val == "" {
189
return nil
190
}
191
if fileutil.FileExists(val) {
192
data, err := os.ReadFile(val)
193
if err != nil {
194
gologger.Fatal().Msgf("failed to read file: %s", err)
195
}
196
return data
197
}
198
return []byte(val)
199
}
200
201
// generateCertWithKey creates a self-signed certificate with the given identifier and private key
202
func (k *KeyHandler) generateCertWithKey(identifier string, privateKey *ecdsa.PrivateKey) ([]byte, error) {
203
// Setting up the certificate
204
notBefore := time.Now()
205
notAfter := notBefore.Add(4 * 365 * 24 * time.Hour)
206
207
serialNumber := big.NewInt(xid.New().Time().Unix())
208
// create certificate template
209
template := x509.Certificate{
210
SerialNumber: serialNumber,
211
Subject: pkix.Name{
212
CommonName: identifier,
213
},
214
SignatureAlgorithm: x509.ECDSAWithSHA256,
215
NotBefore: notBefore,
216
NotAfter: notAfter,
217
PublicKey: &privateKey.PublicKey,
218
KeyUsage: x509.KeyUsageDigitalSignature,
219
ExtKeyUsage: []x509.ExtKeyUsage{
220
x509.ExtKeyUsageServerAuth,
221
},
222
IsCA: false,
223
BasicConstraintsValid: true,
224
}
225
// Create the certificate
226
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
227
if err != nil {
228
return nil, err
229
}
230
231
var certOut bytes.Buffer
232
if err := pem.Encode(&certOut, &pem.Block{Type: CertType, Bytes: derBytes}); err != nil {
233
return nil, err
234
}
235
return certOut.Bytes(), nil
236
}
237
238
// marshalPrivateKey marshals the private key and encrypts it with the given passphrase
239
func (k *KeyHandler) marshalPrivateKey(privateKey *ecdsa.PrivateKey) ([]byte, error) {
240
241
var passphrase []byte
242
// get passphrase to encrypt private key before saving to disk
243
if !noUserPassphrase {
244
fmt.Printf("[*] Enter passphrase (exit to abort): ")
245
passphrase = getPassphrase()
246
}
247
248
// marshal private key
249
privateKeyData, err := x509.MarshalECPrivateKey(privateKey)
250
if err != nil {
251
gologger.Fatal().Msgf("failed to marshal ecdsa private key: %s", err)
252
}
253
// pem encode keys
254
pemBlock := &pem.Block{
255
Type: PrivateKeyType, Bytes: privateKeyData,
256
}
257
// encrypt private key if passphrase is provided
258
if len(passphrase) > 0 {
259
// encode it with passphrase
260
// this function is deprecated since go 1.16 but go stdlib does not want to provide any alternative
261
// see: https://github.com/golang/go/issues/8860
262
encBlock, err := x509.EncryptPEMBlock(rand.Reader, pemBlock.Type, pemBlock.Bytes, passphrase, x509.PEMCipherAES256) // nolint: all
263
if err != nil {
264
gologger.Fatal().Msgf("failed to encrypt private key: %s", err)
265
}
266
pemBlock = encBlock
267
}
268
return pem.EncodeToMemory(pemBlock), nil
269
}
270
271
func getPassphrase() []byte {
272
bin, err := term.ReadPassword(int(os.Stdin.Fd()))
273
if err != nil {
274
gologger.Fatal().Msgf("could not read passphrase: %s", err)
275
}
276
fmt.Println()
277
if string(bin) == "exit" {
278
gologger.Fatal().Msgf("exiting")
279
}
280
fmt.Printf("[*] Enter same passphrase again: ")
281
bin2, err := term.ReadPassword(int(os.Stdin.Fd()))
282
if err != nil {
283
gologger.Fatal().Msgf("could not read passphrase: %s", err)
284
}
285
fmt.Println()
286
// review: should we allow empty passphrase?
287
// we currently allow empty passphrase
288
if string(bin) != string(bin2) {
289
gologger.Fatal().Msgf("passphrase did not match try again")
290
}
291
return bin
292
}
293
294