mirror of https://github.com/daffainfo/nuclei.git
293 lines
8.5 KiB
Go
293 lines
8.5 KiB
Go
package signer
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"math/big"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
"github.com/projectdiscovery/gologger"
|
|
fileutil "github.com/projectdiscovery/utils/file"
|
|
"github.com/rs/xid"
|
|
"golang.org/x/term"
|
|
)
|
|
|
|
const (
|
|
CertType = "PD NUCLEI USER CERTIFICATE"
|
|
PrivateKeyType = "PD NUCLEI USER PRIVATE KEY"
|
|
CertFilename = "nuclei-user.crt"
|
|
PrivateKeyFilename = "nuclei-user-private-key.pem"
|
|
CertEnvVarName = "NUCLEI_USER_CERTIFICATE"
|
|
PrivateKeyEnvName = "NUCLEI_USER_PRIVATE_KEY"
|
|
)
|
|
|
|
var (
|
|
ErrNoCertificate = fmt.Errorf("nuclei user certificate not found")
|
|
ErrNoPrivateKey = fmt.Errorf("nuclei user private key not found")
|
|
SkipGeneratingKeys = false
|
|
noUserPassphrase = false
|
|
)
|
|
|
|
// KeyHandler handles the key generation and management
|
|
// of signer public and private keys
|
|
type KeyHandler struct {
|
|
UserCert []byte
|
|
PrivateKey []byte
|
|
cert *x509.Certificate
|
|
ecdsaPubKey *ecdsa.PublicKey
|
|
ecdsaKey *ecdsa.PrivateKey
|
|
}
|
|
|
|
// ReadUserCert reads the user certificate from environment variable or given directory
|
|
func (k *KeyHandler) ReadCert(envName, dir string) error {
|
|
// read from env
|
|
if cert := k.getEnvContent(envName); cert != nil {
|
|
k.UserCert = cert
|
|
return nil
|
|
}
|
|
// read from disk
|
|
if cert, err := os.ReadFile(filepath.Join(dir, CertFilename)); err == nil {
|
|
k.UserCert = cert
|
|
return nil
|
|
}
|
|
return ErrNoCertificate
|
|
}
|
|
|
|
// ReadPrivateKey reads the private key from environment variable or given directory
|
|
func (k *KeyHandler) ReadPrivateKey(envName, dir string) error {
|
|
// read from env
|
|
if privateKey := k.getEnvContent(envName); privateKey != nil {
|
|
k.PrivateKey = privateKey
|
|
return nil
|
|
}
|
|
// read from disk
|
|
if privateKey, err := os.ReadFile(filepath.Join(dir, PrivateKeyFilename)); err == nil {
|
|
k.PrivateKey = privateKey
|
|
return nil
|
|
}
|
|
return ErrNoPrivateKey
|
|
}
|
|
|
|
// ParseUserCert parses the user certificate and returns the public key
|
|
func (k *KeyHandler) ParseUserCert() error {
|
|
block, _ := pem.Decode(k.UserCert)
|
|
if block == nil {
|
|
return fmt.Errorf("failed to parse PEM block containing the certificate")
|
|
}
|
|
cert, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if cert.Subject.CommonName == "" {
|
|
return fmt.Errorf("invalid certificate: expected common name to be set")
|
|
}
|
|
k.cert = cert
|
|
var ok bool
|
|
k.ecdsaPubKey, ok = cert.PublicKey.(*ecdsa.PublicKey)
|
|
if !ok {
|
|
return fmt.Errorf("failed to parse ecdsa public key from cert")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ParsePrivateKey parses the private key and returns the private key
|
|
func (k *KeyHandler) ParsePrivateKey() error {
|
|
block, _ := pem.Decode(k.PrivateKey)
|
|
if block == nil {
|
|
return fmt.Errorf("failed to parse PEM block containing the private key")
|
|
}
|
|
// if pem block is encrypted , decrypt it
|
|
if x509.IsEncryptedPEMBlock(block) { // nolint: all
|
|
gologger.Info().Msgf("Private Key is encrypted with passphrase")
|
|
fmt.Printf("[*] Enter passphrase (exit to abort): ")
|
|
bin, err := term.ReadPassword(int(os.Stdin.Fd()))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fmt.Println()
|
|
if string(bin) == "exit" {
|
|
return fmt.Errorf("private key requires passphrase, but none was provided")
|
|
}
|
|
block.Bytes, err = x509.DecryptPEMBlock(block, bin) // nolint: all
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
var err error
|
|
k.ecdsaKey, err = x509.ParseECPrivateKey(block.Bytes)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GenerateKeyPair generates a new key-pair for signing code templates
|
|
func (k *KeyHandler) GenerateKeyPair() {
|
|
|
|
gologger.Info().Msgf("Generating new key-pair for signing templates")
|
|
fmt.Printf("[*] Enter User/Organization Name (exit to abort) : ")
|
|
|
|
// get user/organization name
|
|
identifier := ""
|
|
_, err := fmt.Scanln(&identifier)
|
|
if err != nil {
|
|
gologger.Fatal().Msgf("failed to read user/organization name: %s", err)
|
|
}
|
|
if identifier == "exit" {
|
|
gologger.Fatal().Msgf("exiting key-pair generation")
|
|
}
|
|
if identifier == "" {
|
|
gologger.Fatal().Msgf("user/organization name cannot be empty")
|
|
}
|
|
|
|
// generate new key-pair
|
|
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
gologger.Fatal().Msgf("failed to generate ecdsa key-pair: %s", err)
|
|
}
|
|
|
|
// create x509 certificate with user/organization name and public key
|
|
// self-signed certificate with generated private key
|
|
k.UserCert, err = k.generateCertWithKey(identifier, privateKey)
|
|
if err != nil {
|
|
gologger.Fatal().Msgf("failed to create certificate: %s", err)
|
|
}
|
|
|
|
// marshal private key
|
|
k.PrivateKey, err = k.marshalPrivateKey(privateKey)
|
|
if err != nil {
|
|
gologger.Fatal().Msgf("failed to marshal ecdsa private key: %s", err)
|
|
}
|
|
gologger.Info().Msgf("Successfully generated new key-pair for signing templates")
|
|
}
|
|
|
|
// SaveToDisk saves the generated key-pair to the given directory
|
|
func (k *KeyHandler) SaveToDisk(dir string) error {
|
|
_ = fileutil.FixMissingDirs(filepath.Join(dir, CertFilename)) // not required but just in case will take care of missing dirs in path
|
|
if err := os.WriteFile(filepath.Join(dir, CertFilename), k.UserCert, 0600); err != nil {
|
|
return err
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, PrivateKeyFilename), k.PrivateKey, 0600); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getEnvContent returns the content of the environment variable
|
|
// if it is a file then it loads its content
|
|
func (k *KeyHandler) getEnvContent(name string) []byte {
|
|
val := os.Getenv(name)
|
|
if val == "" {
|
|
return nil
|
|
}
|
|
if fileutil.FileExists(val) {
|
|
data, err := os.ReadFile(val)
|
|
if err != nil {
|
|
gologger.Fatal().Msgf("failed to read file: %s", err)
|
|
}
|
|
return data
|
|
}
|
|
return []byte(val)
|
|
}
|
|
|
|
// generateCertWithKey creates a self-signed certificate with the given identifier and private key
|
|
func (k *KeyHandler) generateCertWithKey(identifier string, privateKey *ecdsa.PrivateKey) ([]byte, error) {
|
|
// Setting up the certificate
|
|
notBefore := time.Now()
|
|
notAfter := notBefore.Add(4 * 365 * 24 * time.Hour)
|
|
|
|
serialNumber := big.NewInt(xid.New().Time().Unix())
|
|
// create certificate template
|
|
template := x509.Certificate{
|
|
SerialNumber: serialNumber,
|
|
Subject: pkix.Name{
|
|
CommonName: identifier,
|
|
},
|
|
SignatureAlgorithm: x509.ECDSAWithSHA256,
|
|
NotBefore: notBefore,
|
|
NotAfter: notAfter,
|
|
PublicKey: &privateKey.PublicKey,
|
|
KeyUsage: x509.KeyUsageDigitalSignature,
|
|
ExtKeyUsage: []x509.ExtKeyUsage{
|
|
x509.ExtKeyUsageServerAuth,
|
|
},
|
|
IsCA: false,
|
|
BasicConstraintsValid: true,
|
|
}
|
|
// Create the certificate
|
|
derBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var certOut bytes.Buffer
|
|
if err := pem.Encode(&certOut, &pem.Block{Type: CertType, Bytes: derBytes}); err != nil {
|
|
return nil, err
|
|
}
|
|
return certOut.Bytes(), nil
|
|
}
|
|
|
|
// marshalPrivateKey marshals the private key and encrypts it with the given passphrase
|
|
func (k *KeyHandler) marshalPrivateKey(privateKey *ecdsa.PrivateKey) ([]byte, error) {
|
|
|
|
var passphrase []byte
|
|
// get passphrase to encrypt private key before saving to disk
|
|
if !noUserPassphrase {
|
|
fmt.Printf("[*] Enter passphrase (exit to abort): ")
|
|
passphrase = getPassphrase()
|
|
}
|
|
|
|
// marshal private key
|
|
privateKeyData, err := x509.MarshalECPrivateKey(privateKey)
|
|
if err != nil {
|
|
gologger.Fatal().Msgf("failed to marshal ecdsa private key: %s", err)
|
|
}
|
|
// pem encode keys
|
|
pemBlock := &pem.Block{
|
|
Type: PrivateKeyType, Bytes: privateKeyData,
|
|
}
|
|
// encrypt private key if passphrase is provided
|
|
if len(passphrase) > 0 {
|
|
// encode it with passphrase
|
|
// this function is deprecated since go 1.16 but go stdlib does not want to provide any alternative
|
|
// see: https://github.com/golang/go/issues/8860
|
|
encBlock, err := x509.EncryptPEMBlock(rand.Reader, pemBlock.Type, pemBlock.Bytes, passphrase, x509.PEMCipherAES256) // nolint: all
|
|
if err != nil {
|
|
gologger.Fatal().Msgf("failed to encrypt private key: %s", err)
|
|
}
|
|
pemBlock = encBlock
|
|
}
|
|
return pem.EncodeToMemory(pemBlock), nil
|
|
}
|
|
|
|
func getPassphrase() []byte {
|
|
bin, err := term.ReadPassword(int(os.Stdin.Fd()))
|
|
if err != nil {
|
|
gologger.Fatal().Msgf("could not read passphrase: %s", err)
|
|
}
|
|
fmt.Println()
|
|
if string(bin) == "exit" {
|
|
gologger.Fatal().Msgf("exiting")
|
|
}
|
|
fmt.Printf("[*] Enter same passphrase again: ")
|
|
bin2, err := term.ReadPassword(int(os.Stdin.Fd()))
|
|
if err != nil {
|
|
gologger.Fatal().Msgf("could not read passphrase: %s", err)
|
|
}
|
|
fmt.Println()
|
|
// review: should we allow empty passphrase?
|
|
// we currently allow empty passphrase
|
|
if string(bin) != string(bin2) {
|
|
gologger.Fatal().Msgf("passphrase did not match try again")
|
|
}
|
|
return bin
|
|
}
|