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 } // ReadCert 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 }