/*
  Common functions used by the NSS-aware code in systemtap.

  Copyright (C) 2009-2014 Red Hat Inc.

  This file is part of systemtap, and is free software.  You can
  redistribute it and/or modify it under the terms of the GNU General Public
  License as published by the Free Software Foundation; either version 2 of the
  License, or (at your option) any later version.

  This program is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  GNU General Public License for more details.

  You should have received a copy of the GNU General Public License
  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "config.h"

#include <iostream>
#include <fstream>
#include <sstream>
#include <cerrno>
#include <cstdio>
#include <cassert>

extern "C" {
#include <time.h>
#include <termios.h>
#include <unistd.h>
#include <glob.h>
#include <sys/stat.h>
#include <sys/utsname.h>

#include <nss.h>
#include <nspr.h>
#include <ssl.h>
#include <prerror.h>
#include <secerr.h>
#include <sslerr.h>
#include <cryptohi.h>
#include <keyhi.h>
#include <secder.h>
#include <cert.h>
}

#include "nsscommon.h"
#include "util.h"

using namespace std;

// Common constants and settings.
const char *
server_cert_nickname ()
{
  return (const char *)"stap-server";
}

string
add_cert_db_prefix (const string &db_path) {
#if (NSS_VMAJOR > 3) || (NSS_VMAJOR == 3 && NSS_VMINOR >= 12)
  // Use the dbm prefix, if a prefix is not already specified,
  // since we're using the old database format.
  if (db_path.find (':') == string::npos)
    return string("dbm:") + db_path;
#endif
  return db_path;
}

string
server_cert_db_path ()
{
  string data_path;
  const char* s_d = getenv ("SYSTEMTAP_DIR");
  if (s_d != NULL)
    data_path = s_d;
  else
    data_path = get_home_directory() + string("/.systemtap");
  return data_path + "/ssl/server";
}

string
local_client_cert_db_path ()
{
  string data_path;
  const char* s_d = getenv ("SYSTEMTAP_DIR");
  if (s_d != NULL)
    data_path = s_d;
  else
    data_path = get_home_directory() + string("/.systemtap");
  return data_path + "/ssl/client";
}

// Common error handling for applications using this file.
void
nsscommon_error (const string &msg, int logit)
{
  // Call the extern "C" version supplied by each application.
  nsscommon_error (msg.c_str (), logit);
}

// Logging. Enabled only by stap-serverd but called from some common methods.
static ofstream logfile;

void
start_log (const char *arg)
{
  if (logfile.is_open ())
    logfile.close ();

  logfile.open (arg, ios_base::app);
  if (! logfile.good ())
    nsscommon_error (_F("Could not open log file %s", arg));
}

bool
log_ok ()
{
  return logfile.good ();
}

void
log (const string &msg)
{
  // What time is it?
  time_t now;
  time (& now);
  string nowStr = ctime (& now);
  // Remove the newline from the end of the time string.
  nowStr.erase (nowStr.size () - 1, 1);

  if (logfile.good ())
    logfile << nowStr << ": " << msg << endl << flush;
  else
    clog << nowStr << ": " << msg << endl << flush;
}

void
end_log ()
{
  if (logfile.is_open ())
    logfile.close ();
}

// NSS/NSPR error reporting and cleanup.
// These functions are called from C code as well as C++, so make them extern "C".
extern "C"
void
nssError (void)
{
  // See if PR_GetError can tell us what the error is.
  PRErrorCode errorNumber = PR_GetError ();

  // PR_ErrorToString always returns a valid string for errors in this range.
  if (errorNumber >= PR_NSPR_ERROR_BASE && errorNumber <= PR_MAX_ERROR)
    {
      nsscommon_error (_F("(%d) %s", errorNumber, PR_ErrorToString (errorNumber, PR_LANGUAGE_EN)));
      return;
    }

  // PR_ErrorToString does not handle errors outside the range above, so we handle them ourselves.
  const char *errorText;
  switch (errorNumber) {
  default: errorText = "Unknown error"; break;
#define NSSYERROR(code,msg) case code: errorText = msg; break
#include "stapsslerr.h"
#undef NSSYERROR
    }

  nsscommon_error (_F("(%d) %s", errorNumber, errorText));
}

extern "C"
SECStatus
nssInit (const char *db_path, int readWrite, int issueMessage)
{
  SECStatus secStatus;
  string full_db_path = add_cert_db_prefix (db_path);
  db_path = full_db_path.c_str();
  if (readWrite)
    secStatus = NSS_InitReadWrite (db_path);
  else
    secStatus = NSS_Init (db_path);
  if (secStatus != SECSuccess && issueMessage)
    {
      nsscommon_error (_F("Error initializing NSS for %s", db_path));
      nssError ();
    }
  return secStatus;
}

extern "C"
void
nssCleanup (const char *db_path)
{
  // Make sure that NSS has been initialized. Some early versions of NSS do not check this
  // within NSS_Shutdown().
  // When called with no certificate database path (db_path == 0), then the caller does
  // not know whether NSS has actually been initialized. For example, the rpm finder,
  // which calls here to shutdown NSS manually if rpmFreeCrypto() is not available
  // (see rpm_finder.cxx:missing_rpm_enlist).
  // However, if we're trying to close a certificate database which has not been initialized,
  // then we have a (non fatal) internal error.
  if (! NSS_IsInitialized ())
    {
      if (db_path)
	{
	  string full_db_path = add_cert_db_prefix (db_path);
	  db_path = full_db_path.c_str();
	  nsscommon_error (_F("WARNING: Attempt to shutdown NSS for database %s, which was never initialized", db_path));
	}
      return;
    }

  // Shutdown NSS and ensure that it went down successfully. This is because we can not
  // initialize NSS again if it does not.
  if (NSS_Shutdown () != SECSuccess)
    {
      if (db_path)
	{
	  string full_db_path = add_cert_db_prefix (db_path);
	  db_path = full_db_path.c_str();
	  nsscommon_error (_F("Unable to shutdown NSS for database %s", db_path));
	}
      else
	nsscommon_error (_("Unable to shutdown NSS"));
      nssError ();
    }
}

// Certificate database password support functions.
//
// Disable character echoing, if the fd is a tty.
static void
echoOff(int fd)
{
  if (isatty(fd)) {
    struct termios tio;
    tcgetattr(fd, &tio);
    tio.c_lflag &= ~ECHO;
    tcsetattr(fd, TCSAFLUSH, &tio);
  }
}

/* Enable character echoing, if the fd is a tty.  */
static void
echoOn(int fd)
{
  if (isatty(fd)) {
    struct termios tio;
    tcgetattr(fd, &tio);
    tio.c_lflag |= ECHO;
    tcsetattr(fd, TCSAFLUSH, &tio);
  }
}

/*
 * This function is our custom password handler that is called by
 * NSS when retrieving private certs and keys from the database. Returns a
 * pointer to a string with a password for the database. Password pointer
 * must be allocated by one of the NSPR memory allocation functions, or by PORT_Strdup,
 * and will be freed by the caller.
 */
extern "C"
char *
nssPasswordCallback (PK11SlotInfo *info __attribute ((unused)), PRBool retry, void *arg)
{
  static int retries = 0;
  #define PW_MAX 200
  char* password = NULL;
  char* password_ret = NULL;
  const char *dbname ;
  int infd;
  int isTTY;

  if (! retry)
    {
      /* Not a retry. */
      retries = 0;
    }
  else
    {
      /* Maximum of 2 retries for bad password.  */
      if (++retries > 2)
	return NULL; /* No more retries */
    }

  /* Can only prompt for a password if stdin is a tty.  */
  infd = fileno (stdin);
  isTTY = isatty (infd);
  if (! isTTY)
    {
      nsscommon_error (_("Cannot prompt for certificate database password. stdin is not a tty"));
      return NULL;
    }

  /* Prompt for password */
  password = (char *)PORT_Alloc (PW_MAX);
  if (! password)
    {
      nssError ();
      return NULL;
    }

  dbname = (const char *)arg;
  cerr << _F("Password for certificate database in %s: ", dbname) << flush;
  echoOff (infd);
  password_ret = fgets (password, PW_MAX, stdin);
  cerr << endl << flush;
  echoOn(infd);

  if (password_ret)
    /* stomp on the newline */
    *strchrnul (password, '\n') = '\0';
  else
    PORT_Free (password);

  return password_ret;
}

static int
create_server_cert_db (const char *db_path)
{
  return create_dir (db_path, 0755);
}

static int
create_client_cert_db (const char *db_path)
{
  // Same properties as the server's database, at present.
  return create_server_cert_db (db_path);
}

static int
clean_cert_db (const string &db_path)
{
  // First remove all files from the directory
  glob_t globbuf;
  string filespec = db_path + "/*";
  int r = glob (filespec.c_str (), 0, NULL, & globbuf);
  if (r == GLOB_NOSPACE || r == GLOB_ABORTED)
    nsscommon_error (_F("Could not search certificate database directory %s", db_path.c_str ()));
  else if (r != GLOB_NOMATCH)
    {
      for (unsigned i = 0; i < globbuf.gl_pathc; ++i)
	{
	  if (remove_file_or_dir (globbuf.gl_pathv[i]) != 0)
	    nsscommon_error (_F("Could not remove %s", globbuf.gl_pathv[i]));
	}
      globfree(& globbuf);
    }

  // Now remove the directory itself.
  if (remove_file_or_dir (db_path.c_str ()) != 0)
    {
      nsscommon_error (_F("Could not remove certificate database directory %s\n%s",
			  db_path.c_str (), strerror (errno)));
      return 1;
    }

  return 0;
}

static int
init_password (PK11SlotInfo *slot, const string &db_path, bool use_password)
{
  // Prompt for the database password, if we're using one. Keep the passwords in memory for as
  // little time as possible.
  SECStatus secStatus;
  if (use_password)
    {
      char *pw1 = 0;
      int attempts;
      const int max_attempts = 3;
      for (attempts = 0; attempts < max_attempts; ++attempts)
	{
	  pw1 = nssPasswordCallback (slot, false, (void*)db_path.c_str ());
	  if (! pw1)
	    continue;
	  cerr << "Confirm ";
	  bool match = false;
	  char *pw2 = nssPasswordCallback (slot, false, (void*)db_path.c_str ());
	  if (pw2)
	    {
	      if (strcmp (pw1, pw2) == 0)
		match = true;
	      else
		nsscommon_error (_("Passwords do not match"));
	      memset (pw2, 0, strlen (pw2));
	      PORT_Free (pw2);
	    }
	  if (match)
	    break;
	  memset (pw1, 0, strlen (pw1));
	  PORT_Free (pw1);
	}
      if (attempts >= max_attempts)
	{
	  nsscommon_error (_("Too many password attempts"));
	  return 1;
	}
      secStatus = PK11_InitPin (slot, 0, pw1);
      memset (pw1, 0, strlen (pw1));
      PORT_Free (pw1);
    }
  else
    secStatus = PK11_InitPin (slot, 0, 0);

  if (secStatus != SECSuccess)
    {
      nsscommon_error (_F("Could not initialize pin for certificate database %s", db_path.c_str()));
      nssError ();
      return 1;
    }

  return 0;
}

static SECKEYPrivateKey *
generate_private_key (const string &db_path, PK11SlotInfo *slot, SECKEYPublicKey **pubkeyp)
{
  if (PK11_Authenticate (slot, PR_TRUE, 0) != SECSuccess)
    {
      nsscommon_error (_F("Unable to authenticate the default slot for certificate database %s",
			  db_path.c_str ()));
      nssError ();
      return 0;
    }

  // Do some random-number initialization.
  // TODO: We can do better.
  srand (time (NULL));
  char randbuf[64];
  for (unsigned i = 0; i < sizeof (randbuf); ++i)
    randbuf[i] = rand ();
  PK11_RandomUpdate (randbuf, sizeof (randbuf));
  memset (randbuf, 0, sizeof (randbuf));

  // Set up for RSA.
  PK11RSAGenParams rsaparams;
  rsaparams.keySizeInBits = 1024;
  rsaparams.pe = 0x010001;
  CK_MECHANISM_TYPE mechanism = CKM_RSA_PKCS_KEY_PAIR_GEN;

  // Generate the key pair.
  SECKEYPrivateKey *privKey = PK11_GenerateKeyPair (slot, mechanism, & rsaparams, pubkeyp,
						    PR_TRUE /*isPerm*/, PR_TRUE /*isSensitive*/,
						    0/*pwdata*/);
  if (! privKey)
    {
      nsscommon_error (_("Unable to generate public/private key pair"));
      nssError ();
    }
  return privKey;
}

static CERTCertificateRequest *
generate_cert_request (SECKEYPublicKey *pubk, CERTName *subject)
{
  CERTSubjectPublicKeyInfo *spki = SECKEY_CreateSubjectPublicKeyInfo (pubk);
  if (! spki)
    {
      nsscommon_error (_("Unable to create subject public key info for certificate request"));
      nssError ();
      return 0;
    }
  
  /* Generate certificate request */
  CERTCertificateRequest *cr = CERT_CreateCertificateRequest (subject, spki, 0);
  SECKEY_DestroySubjectPublicKeyInfo (spki);
  if (! cr)
    {
      nsscommon_error (_("Unable to create certificate request"));
      nssError ();
    }
  return cr;
}

static CERTCertificate *
create_cert (CERTCertificateRequest *certReq, const string &dnsNames)
{
  // What is the current date and time?
  PRTime now = PR_Now ();

  // What is the date and time 1 year from now?
  PRExplodedTime printableTime;
  PR_ExplodeTime (now, PR_GMTParameters, & printableTime);
  printableTime.tm_month += 12;
  PRTime after = PR_ImplodeTime (& printableTime);
 
  // Note that the time is now in micro-second units.
  CERTValidity *validity = CERT_CreateValidity (now, after);
  if (! validity)
    {
      nsscommon_error (_("Unable to create certificate validity dates"));
      nssError ();
      return 0;
    }

  // Create a default serial number using the current time.
  PRTime serialNumber = now >> 19; // copied from certutil.

  // Create the certificate.
  CERTCertificate *cert = CERT_CreateCertificate (serialNumber, & certReq->subject, validity,
						  certReq);
  CERT_DestroyValidity (validity);
  if (! cert)
    {
      nsscommon_error (_("Unable to create certificate"));
      nssError ();
      return 0;
    }

  // Predeclare these to keep C++ happy about jumps to the label 'error'.
  SECStatus secStatus = SECSuccess;
  unsigned char keyUsage = 0x0;
  PRArenaPool *arena = 0;

  // Add the extensions that we need.
  void *extHandle = CERT_StartCertExtensions (cert);
  if (! extHandle)
    {
      nsscommon_error (_("Unable to allocate certificate extensions"));
      nssError ();
      goto error;
    }

  // Cert type extension.
  keyUsage |= (0x80 >> 1); // SSL Server
  keyUsage |= (0x80 >> 3); // Object signer
  keyUsage |= (0x80 >> 7); // Object signing CA

  SECItem bitStringValue;
  bitStringValue.data = & keyUsage;
  bitStringValue.len = 1;

  secStatus = CERT_EncodeAndAddBitStrExtension (extHandle,
						SEC_OID_NS_CERT_EXT_CERT_TYPE,
						& bitStringValue, PR_TRUE);
  if (secStatus != SECSuccess)
    {
      nsscommon_error (_("Unable to encode certificate type extensions"));
      nssError ();
      goto error;
    }

  // Alternate dns name extension.
  if (! dnsNames.empty ())
    {
      arena = PORT_NewArena (DER_DEFAULT_CHUNKSIZE);
      if (! arena)
	{
	  nsscommon_error (_("Unable to allocate alternate DNS name extension for certificate"));
	  goto error;
	}

      // Walk down the comma separated list of names.
      CERTGeneralName *nameList = 0;
      CERTGeneralName *current = 0;
      PRCList *prev = 0;
      vector<string>components;
      tokenize (dnsNames, components, ",");
      for (unsigned i = 0; i < components.size (); ++i)
	{
	  char *tbuf = (char *)PORT_ArenaAlloc (arena, components[i].size () + 1);
	  strcpy (tbuf, components[i].c_str ());

	  current = (CERTGeneralName *)PORT_ZAlloc (sizeof (CERTGeneralName));
	  if (! current)
	    {
	      nsscommon_error (_("Unable to allocate alternate DNS name extension for certificate"));
	      goto error;
	    }
	  if (prev)
	    {
	      current->l.prev = prev;
	      prev->next = & current->l;
	    }
	  else
	    nameList = current;

	  current->type = certDNSName;
	  current->name.other.data = (unsigned char *)tbuf;
	  current->name.other.len = strlen (tbuf);
	  prev = & current->l;
	}

      // At this point nameList points to the head of a doubly linked,
      // but not yet circular, list and current points to its tail.
      if (nameList)
	{
	  // Make nameList circular.
	  nameList->l.prev = prev;
	  current->l.next = & nameList->l;

	  // Encode and add the extension.
	  SECItem item;
	  secStatus = CERT_EncodeAltNameExtension (arena, nameList, & item);
	  if (secStatus != SECSuccess)
	    {
	      nsscommon_error (_("Unable to encode alternate DNS name extension for certificate"));
	      nssError ();
	      goto error;
	    }
	  secStatus = CERT_AddExtension(extHandle,
					SEC_OID_X509_SUBJECT_ALT_NAME,
					& item, PR_FALSE, PR_TRUE);
	  if (secStatus != SECSuccess)
	    {
	      nsscommon_error (_("Unable to add alternate DNS name extension for certificate"));
	      nssError ();
	      goto error;
	    }
	}
    } // extra dns names specified.

  // We did not create any extensions on the cert request.
  assert (certReq->attributes != NULL);
  assert (certReq->attributes[0] == NULL);

  // Finished with cert extensions.
  secStatus = CERT_FinishExtensions (extHandle);
  if (secStatus != SECSuccess)
    {
      nsscommon_error (_("Unable to complete alternate DNS name extension for certificate"));
      nssError ();
      goto error;
    }

  return cert;

 error:
  if (arena)
    PORT_FreeArena (arena, PR_FALSE);
  CERT_DestroyCertificate (cert);
  return 0;
}

static SECItem *
sign_cert (CERTCertificate *cert, SECKEYPrivateKey *privKey)
{
  SECOidTag algID = SEC_GetSignatureAlgorithmOidTag (privKey->keyType,
						     SEC_OID_UNKNOWN);
  if (algID == SEC_OID_UNKNOWN)
    {
      nsscommon_error (_("Unable to determine the signature algorithm for the signing the certificate"));
      nssError ();
      return 0;
    }

  PRArenaPool *arena = cert->arena;
  SECStatus rv = SECOID_SetAlgorithmID (arena, & cert->signature, algID, 0);
  if (rv != SECSuccess)
    {
      nsscommon_error (_("Unable to set the signature algorithm for signing the certificate"));
      nssError ();
      return 0;
    }

  /* we only deal with cert v3 here */
  *(cert->version.data) = 2;
  cert->version.len = 1;

  SECItem der;
  der.len = 0;
  der.data = 0;
  void *dummy = SEC_ASN1EncodeItem (arena, & der, cert,
				    SEC_ASN1_GET (CERT_CertificateTemplate));
  if (! dummy)
    {
      nsscommon_error (_("Unable to encode the certificate for signing"));
      nssError ();
      return 0;
    }

  SECItem *result = (SECItem *)PORT_ArenaZAlloc (arena, sizeof (SECItem));
  if (! result)
    {
      nsscommon_error (_("Unable to allocate memory for signing the certificate"));
      return 0;
    }

  rv = SEC_DerSignData (arena, result, der.data, der.len, privKey, algID);
  if (rv != SECSuccess)
    {
      nsscommon_error (_("Unable to sign the certificate"));
      nssError ();
      return 0;
    }

  cert->derCert = *result;
  return result;
}

static SECStatus
add_server_cert (const string &db_path, SECItem *certDER, PK11SlotInfo *slot)
{
  // Decode the cert.
  CERTCertificate *cert = CERT_DecodeCertFromPackage((char *)certDER->data, certDER->len);
  if (! cert)
    {
      nsscommon_error (_("Unable to decode certificate"));
      nssError ();
      return SECFailure;
    }

  // Import it into the database.
  CERTCertDBHandle *handle = 0;
  CERTCertTrust *trust = NULL;
  SECStatus secStatus = PK11_ImportCert (slot, cert, CK_INVALID_HANDLE,
					 server_cert_nickname (), PR_FALSE);
  if (secStatus != SECSuccess)
    {
      nsscommon_error (_F("Unable to import certificate into the database at %s", db_path.c_str ()));
      nssError ();
      goto done;
    }
  
  // Make it a trusted server and signer.
  trust = (CERTCertTrust *)PORT_ZAlloc (sizeof (CERTCertTrust));
  if (! trust)
    {
      nsscommon_error (_("Unable to allocate certificate trust"));
      secStatus = SECFailure;
      goto done;
    }

  secStatus = CERT_DecodeTrustString (trust, "PCu,,PCu");
  if (secStatus != SECSuccess)
    {
      nsscommon_error (_("Unable decode trust string 'PCu,,PCu'"));
      nssError ();
      goto done;
    }
    
  handle = CERT_GetDefaultCertDB ();
  assert (handle);
  secStatus = CERT_ChangeCertTrust (handle, cert, trust);
  if (secStatus != SECSuccess)
    {
      nsscommon_error (_("Unable to change certificate trust"));
      nssError ();
    }

done:
  CERT_DestroyCertificate (cert);
  if (trust)
    PORT_Free (trust);
  return secStatus;
}

SECStatus
add_client_cert (const string &inFileName, const string &db_path)
{
  FILE *inFile = fopen (inFileName.c_str (), "rb");
  if (! inFile)
    {
      nsscommon_error (_F("Could not open certificate file %s for reading\n%s",
			  inFileName.c_str (), strerror (errno)));
      return SECFailure;
    }

  int fd = fileno (inFile);
  struct stat info;
  int rc = fstat (fd, &info);
  if (rc != 0)
    {
      nsscommon_error (_F("Could not obtain information about certificate file %s\n%s",
			  inFileName.c_str (), strerror (errno)));
      fclose (inFile);
      return SECFailure;
    }

  SECItem certDER;
  certDER.len = info.st_size;
  certDER.data = (unsigned char *)PORT_Alloc (certDER.len);
  if (certDER.data == NULL)
    {
      nsscommon_error (_F("Could not allocate certDER\n%s",
			  strerror (errno)));
      fclose (inFile);
      return SECFailure;
    }
  size_t read = fread (certDER.data, 1, certDER.len, inFile);
  fclose (inFile);
  if (read != certDER.len)
    {
      nsscommon_error (_F("Error reading from certificate file %s\n%s",
			  inFileName.c_str (), strerror (errno)));
      return SECFailure;
    }

  // See if the database already exists and can be initialized.
  SECStatus secStatus = nssInit (db_path.c_str (), 1/*readwrite*/, 0/*issueMessage*/);
  if (secStatus != SECSuccess)
    {
      // Try again with a fresh database.
      if (clean_cert_db (db_path.c_str ()) != 0)
	{
	  // Message already issued.
	  return SECFailure;
	}

      // Make sure the given path exists.
      if (create_client_cert_db (db_path.c_str ()) != 0)
	{
	  nsscommon_error (_F("Could not create certificate database directory %s",
			      db_path.c_str ()));
	  return SECFailure;
	}

      // Initialize the new database.
      secStatus = nssInit (db_path.c_str (), 1/*readwrite*/);
      if (secStatus != SECSuccess)
	{
	  // Message already issued.
	  return SECFailure;
	}
    }

  // Predeclare these to keep C++ happy about jumps to the label 'done'.
  CERTCertificate *cert = 0;
  CERTCertDBHandle *handle = 0;
  CERTCertTrust *trust = 0;
  PK11SlotInfo *slot = 0;

  // Add the cert to the database
  // Decode the cert.
  secStatus = SECFailure;
  cert = CERT_DecodeCertFromPackage ((char *)certDER.data, certDER.len);
  if (! cert)
    {
      nsscommon_error (_("Unable to decode certificate"));
      nssError ();
      goto done;
    }

  // We need the internal slot for this database.
  slot = PK11_GetInternalKeySlot ();
  if (! slot)
    {
      nsscommon_error (_F("Could not obtain internal key slot for certificate database %s", db_path.c_str()));
      nssError ();
      goto done;
    }

  // Import it into the database.
  secStatus = PK11_ImportCert (slot, cert, CK_INVALID_HANDLE,
			       server_cert_nickname (), PR_FALSE);
  if (secStatus != SECSuccess)
    {
      nsscommon_error (_F("Could not import certificate into the database at %s", db_path.c_str()));
      nssError ();
      goto done;
    }
  
  // Make it a trusted SSL peer.
  trust = (CERTCertTrust *)PORT_ZAlloc (sizeof (CERTCertTrust));
  if (! trust)
    {
      nsscommon_error (_("Could not allocate certificate trust"));
      goto done;
    }

  secStatus = CERT_DecodeTrustString (trust, "P,P,P");
  if (secStatus != SECSuccess)
    {
      nsscommon_error (_("Unable decode trust string 'P,P,P'"));
      nssError ();
      goto done;
    }

  handle = CERT_GetDefaultCertDB ();
  assert (handle);
  secStatus = CERT_ChangeCertTrust (handle, cert, trust);
  if (secStatus != SECSuccess)
    {
      nsscommon_error (_("Unable to change certificate trust"));
      nssError ();
    }

 done:
  // Free NSS/NSPR objects and shutdown NSS.
  if (slot)
    PK11_FreeSlot (slot);
  if (trust)
    PORT_Free (trust);
  if (cert)
    CERT_DestroyCertificate (cert);
  if (certDER.data)
    PORT_Free (certDER.data);
  nssCleanup (db_path.c_str ());

  // Make sure that the cert database files are read/write by the owner and
  // readable by all.
  glob_t globbuf;
  string filespec = db_path + "/*";
  int r = glob (filespec.c_str (), 0, NULL, & globbuf);
  if (r == GLOB_NOSPACE || r == GLOB_ABORTED) {
    // Not fatal, just a warning
    nsscommon_error (_F("Could not search certificate database directory %s", db_path.c_str ()));
  }
  else if (r != GLOB_NOMATCH)
    {
      mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
      for (unsigned i = 0; i < globbuf.gl_pathc; ++i)
	{
	  // Not fatal, just a warning
	  if (chmod (globbuf.gl_pathv[i], mode) != 0)
	    nsscommon_error (_F("Could set file permissions for %s", globbuf.gl_pathv[i]));
	}
      globfree(&globbuf);
    }
  
  return secStatus;
}

int
gen_cert_db (const string &db_path, const string &extraDnsNames, bool use_password)
{
  // Log the generation of a new database.
  log (_F("Generating a new certificate database directory in %s",
	  db_path.c_str ()));

  // Start with a clean cert database.
  if (clean_cert_db (db_path.c_str ()) != 0)
    {
      // Message already issued.
      return 1;
    }

  // Make sure the given path exists.
  if (create_server_cert_db (db_path.c_str ()) != 0)
    {
      nsscommon_error (_F("Could not create certificate database directory %s",
			  db_path.c_str ()));
      return 1;
    }

  // Initialize the new database.
  SECStatus secStatus = nssInit (db_path.c_str (), 1/*readwrite*/);
  if (secStatus != SECSuccess)
    {
      // Message already issued.
      return 1;
    }

  // Pre declare these to keep g++ happy about jumps to the label 'error'.
  CERTName *subject = 0;
  SECKEYPublicKey *pubkey = 0;
  SECKEYPrivateKey *privkey = 0;
  CERTCertificateRequest *cr = 0;
  CERTCertificate *cert = 0;
  SECItem *certDER = 0;
  string dnsNames;
  string hostname;
  int rc;
  string outFileName;
  FILE *outFile = 0;

  // We need the internal slot for this database.
  PK11SlotInfo *slot = PK11_GetInternalKeySlot ();
  if (! slot)
    {
      nsscommon_error (_F("Could not obtain internal key slot for certificate database %s", db_path.c_str()));
      nssError ();
      goto error;
    }

  // Establish the password (if any) for the new database.
  rc = init_password (slot, db_path, use_password);
  if (rc != 0)
    {
      // Messages already issued.
      goto error;
    }

  // Format the cert subject.
  subject = CERT_AsciiToName ((char *)"CN=Systemtap Compile Server, OU=Systemtap");
  if (! subject)
    {
      nsscommon_error (_("Unable to encode certificate common header"));
      nssError ();
      goto error;
    }

  // Next, generate the private key.
  privkey = generate_private_key (db_path, slot, & pubkey);
  if (! privkey)
    {
      // Message already issued.
      goto error;
    }

  // Next, generate a cert request.
  cr = generate_cert_request (pubkey, subject);
  if (! cr)
    {
      // Message already issued.
      goto error;
    }

  // For the cert, we need our host name.
  struct utsname utsname;
  uname (& utsname);
  dnsNames = utsname.nodename;

  // Because avahi identifies hosts using a ".local" domain, add one to the list of names.
  hostname = dnsNames.substr (0, dnsNames.find ('.'));
  dnsNames += string(",") + hostname + ".local";

  // Add any extra names that were supplied.
  if (! extraDnsNames.empty ())
    dnsNames += "," + extraDnsNames;

  // Now, generate the cert.
  cert = create_cert (cr, dnsNames);
  CERT_DestroyCertificateRequest (cr);
  if (! cert)
    {
      // NSS error already issued.
      nsscommon_error (_("Unable to create certificate"));
      goto error;
    }

  // Sign the cert.
  certDER = sign_cert (cert, privkey);
  if (! certDER)
    {
      // Message already issued.
      goto error;
    }

  // Now output it to a file.
  outFileName = db_path + "/stap.cert";
  outFile = fopen (outFileName.c_str (), "wb");
  if (outFile)
    {
      size_t written = fwrite (certDER->data, 1, certDER->len, outFile);
      if (written != certDER->len)
	{
	  nsscommon_error (_F("Error writing to certificate file %s\n%s",
			      outFileName.c_str (), strerror (errno)));
	}
      fclose (outFile);
    }
  else
    {
      nsscommon_error (_F("Could not open certificate file %s for writing\n%s",
			  outFileName.c_str (), strerror (errno)));
    }

  // Add the cert to the database
  secStatus = add_server_cert (db_path, certDER, slot);
  CERT_DestroyCertificate (cert);
  if (secStatus != SECSuccess)
    {
      // NSS error already issued.
      nsscommon_error (_F("Unable to add certificate to %s", db_path.c_str ()));
      goto error;
    }

  // Done with the certificate database
  PK11_FreeSlot (slot);
  CERT_DestroyName (subject);
  SECKEY_DestroyPublicKey (pubkey);
  SECKEY_DestroyPrivateKey (privkey);
  goto done;

 error:
  if (slot)
    PK11_FreeSlot (slot);
  if (subject)
    CERT_DestroyName (subject);
  if (pubkey)
    SECKEY_DestroyPublicKey (pubkey);
  if (privkey)
    SECKEY_DestroyPrivateKey (privkey);
  if (cert)
    CERT_DestroyCertificate (cert); // Also destroys certDER.

 done:
  nssCleanup (db_path.c_str ());
  return secStatus != SECSuccess;
}

CERTCertList *get_cert_list_from_db (const string &cert_nickname)
{
  // Search the client-side database of trusted servers.
  CERTCertDBHandle *handle = CERT_GetDefaultCertDB ();
  assert (handle);
  CERTCertificate *db_cert = PK11_FindCertFromNickname (cert_nickname.c_str (), 0);
  if (! db_cert)
    {
      // No trusted servers. Not an error. Just an empty list returned.
      return 0;
    }

  // Here, we have one cert with the desired nickname.
  // Now, we will attempt to get a list of ALL certs 
  // with the same subject name as the cert we have.  That list 
  // should contain, at a minimum, the one cert we have already found.
  // If the list of certs is empty (0), the libraries have failed.
  CERTCertList *certs = CERT_CreateSubjectCertList (0, handle, & db_cert->derSubject,
						    PR_Now (), PR_FALSE);
  CERT_DestroyCertificate (db_cert);
  if (! certs)
    {
      nsscommon_error (_("NSS library failure in CERT_CreateSubjectCertList"));
      nssError ();
    }

  return certs;
}

static int
format_cert_validity_time (SECItem &vTime, char *timeString, size_t ts_size)
{
  int64 time;
  SECStatus secStatus;

  switch (vTime.type) {
  case siUTCTime:
    secStatus = DER_UTCTimeToTime (& time, & vTime);
    break;
  case siGeneralizedTime:
    secStatus = DER_GeneralizedTimeToTime (& time, & vTime);
    break;
  default:
    nsscommon_error (_("Could not decode certificate validity"));
    return 1;
  }
  if (secStatus != SECSuccess)
    {
      nsscommon_error (_("Could not decode certificate validity time"));
      return 1;
    }

  // Convert to local time.
  PRExplodedTime printableTime;
  PR_ExplodeTime (time, PR_GMTParameters, & printableTime);
  if (! PR_FormatTime (timeString, ts_size, "%a %b %d %H:%M:%S %Y", & printableTime))
    {
      nsscommon_error (_("Could not format certificate validity time"));
      return 1;
    }

  return 0;
}

static bool
cert_is_valid (CERTCertificate *cert)
{
  // Verify the the certificate is valid as an SSL server and as an object signer and that
  // it is valid now.
  CERTCertDBHandle *handle = CERT_GetDefaultCertDB ();
  assert (handle);
  SECCertificateUsage usage = certificateUsageSSLServer | certificateUsageObjectSigner;
  SECStatus secStatus = CERT_VerifyCertificate (handle, cert, PR_TRUE/*checkSig*/, usage,
						PR_Now (), NULL, NULL/*log*/, & usage);
  return secStatus == SECSuccess;
}

static bool
cert_db_is_valid (const string &db_path, const string &nss_cert_name)
{
  // Make sure the given path exists.
  if (! file_exists (db_path))
    {
      log (_F("Certificate database %s does not exist", db_path.c_str ()));
      return false;
    }

  // If a 'pw' file exists, then this is an old database. Treat any certs as invalid.
  if (file_exists (db_path + "/pw"))
    {
      log (_F("Certificate database %s is obsolete", db_path.c_str ()));
      return false;
    }

  // Initialize the NSS libraries -- readonly
  SECStatus secStatus = nssInit (db_path.c_str ());
  if (secStatus != SECSuccess)
    {
      // Message already issued.
      return false;
    }

  // Obtain a list of our certs from the database.
  bool valid_p = false;
  CERTCertList *certs = get_cert_list_from_db (nss_cert_name);
  if (! certs)
    {
      log (_F("No certificate found in database %s", db_path.c_str ()));
      goto done;
    }

  log (_F("Certificate found in database %s", db_path.c_str ()));
  for (CERTCertListNode *node = CERT_LIST_HEAD (certs);
       ! CERT_LIST_END (node, certs);
       node = CERT_LIST_NEXT (node))
    {
      // The certificate we're working with.
      CERTCertificate *c = node->cert;

      // Print the validity dates of the certificate.
      CERTValidity &v = c->validity;
      char timeString[256];
      if (format_cert_validity_time (v.notBefore, timeString, sizeof (timeString)) == 0)
	log (_F("  Not Valid Before: %s UTC", timeString));
      if (format_cert_validity_time (v.notAfter, timeString, sizeof (timeString)) == 0)
	log (_F("  Not Valid After: %s UTC", timeString));

      // Now ask NSS to check the validity.
      if (cert_is_valid (c))
	{
	  // The cert is valid. One valid cert is enough.
	  log (_("Certificate is valid"));
	  valid_p = true;
	  break;
	}

      // The cert is not valid. Look for another one.
      log (_("Certificate is not valid"));
    }
  CERT_DestroyCertList (certs);

 done:
  nssCleanup (db_path.c_str ());
  return valid_p;
}

// Ensure that our certificate exists and is valid. Generate a new one if not.
int
check_cert (const string &db_path, const string &nss_cert_name, bool use_db_password)
{
  // Generate a new cert database if the current one does not exist or is not valid.
  if (! cert_db_is_valid (db_path, nss_cert_name))
    {
      if (gen_cert_db (db_path, "", use_db_password) != 0)
	{
	  // NSS message already issued.
	  nsscommon_error (_("Unable to generate new certificate"));
	  return 1;
	}
    }
  return 0;
}

void sign_file (
  const string &db_path,
  const string &nss_cert_name,
  const string &inputName,
  const string &outputName
) {
  /* Get own certificate and private key. */
  CERTCertificate *cert = PK11_FindCertFromNickname (nss_cert_name.c_str (), NULL);
  if (cert == NULL)
    {
      nsscommon_error (_F("Unable to find certificate with nickname %s in %s.",
			  nss_cert_name.c_str (), db_path.c_str()));
      nssError ();
      return;
    }

  // Predeclare these to keep C++ happy abount branches to 'done'.
  unsigned char buffer[4096];
  PRFileDesc *local_file_fd = NULL;
  PRInt32 numBytes;
  SECStatus secStatus;
  SGNContext *sgn;
  SECItem signedData;

  /* db_path.c_str () gets passed to nssPasswordCallback */
  SECKEYPrivateKey *privKey = PK11_FindKeyByAnyCert (cert, (void *)db_path.c_str ());
  if (privKey == NULL)
    {
      nsscommon_error (_F("Unable to obtain private key from the certificate with nickname %s in %s.",
			  nss_cert_name.c_str (), db_path.c_str()));
      nssError ();
      goto done;
    }

  /* Sign the file. */
  /* Set up the signing context.  */
  sgn = SGN_NewContext (SEC_OID_PKCS1_SHA1_WITH_RSA_ENCRYPTION, privKey);
  if (! sgn) 
    {
      nsscommon_error (_("Could not create signing context"));
      nssError ();
      return;
    }
  secStatus = SGN_Begin (sgn);
  if (secStatus != SECSuccess)
    {
      nsscommon_error (_("Could not initialize signing context."));
      nssError ();
      return;
    }

  /* Now read the data and add it to the signature.  */
  local_file_fd = PR_Open (inputName.c_str(), PR_RDONLY, 0);
  if (local_file_fd == NULL)
    {
      nsscommon_error (_F("Could not open module file %s", inputName.c_str ()));
      nssError ();
      return;
    }

  for (;;)
    {
      // No need for PR_Read_Complete here, since we're already managing multiple
      // reads to a fixed size buffer.
      numBytes = PR_Read (local_file_fd, buffer, sizeof (buffer));
      if (numBytes == 0)
	break;	/* EOF */

      if (numBytes < 0)
	{
	  nsscommon_error (_F("Error reading module file %s", inputName.c_str ()));
	  nssError ();
	  goto done;
	}

      /* Add the data to the signature.  */
      secStatus = SGN_Update (sgn, buffer, numBytes);
      if (secStatus != SECSuccess)
	{
	  nsscommon_error (_F("Error while signing module file %s", inputName.c_str ()));
	  nssError ();
	  goto done;
	}
    }

  /* Complete the signature.  */
  secStatus = SGN_End (sgn, & signedData);
  if (secStatus != SECSuccess)
    {
      nsscommon_error (_F("Could not complete signature of module file %s", inputName.c_str ()));
      nssError ();
      goto done;
    }

  SGN_DestroyContext (sgn, PR_TRUE);

  /* Now write the signed data to the output file.  */
  if(local_file_fd != NULL)
    PR_Close (local_file_fd);
  local_file_fd = PR_Open (outputName.c_str(), PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
			   PR_IRUSR | PR_IWUSR | PR_IRGRP | PR_IWGRP | PR_IROTH);
  if (local_file_fd == NULL)
    {
      nsscommon_error (_F("Could not open signature file %s", outputName.c_str ()));
      nssError ();
      goto done;
    }

  numBytes = PR_Write (local_file_fd, signedData.data, signedData.len);
  if (numBytes < 0 || numBytes != (PRInt32)signedData.len)
    {
      nsscommon_error (_F("Error writing to signature file %s", outputName.c_str ()));
      nssError ();
    }

 done:
  if (privKey)
    SECKEY_DestroyPrivateKey (privKey);
  CERT_DestroyCertificate (cert);
  if(local_file_fd != NULL)
    PR_Close (local_file_fd);
}

// PR_Read() is not guaranteed to read all of the requested data in one call.
// Iterate until all of the requested data has been read.
// Return the same values as PR_Read() would.
PRInt32 PR_Read_Complete (PRFileDesc *fd, void *buf, PRInt32 requestedBytes)
{
  // Read until EOF or until the expected number of bytes has been read.
  // PR_Read wants (void*), but we need (char *) to do address arithmetic.
  char *buffer = (char *)buf;
  PRInt32 totalBytes;
  PRInt32 bytesRead;
  for (totalBytes = 0; totalBytes < requestedBytes; totalBytes += bytesRead)
    {
      // Now read the data.
      bytesRead = PR_Read (fd, (void *)(buffer + totalBytes), requestedBytes - totalBytes);
      if (bytesRead == 0)
	break;	// EOF
      if (bytesRead < 0)
	return bytesRead; // Error
    }

  // Return the number of bytes we managed to read.
  return totalBytes;
}

SECStatus
read_cert_info_from_file (const string &certPath, string &fingerprint)
{
  FILE *certFile = fopen (certPath.c_str (), "rb");
  SECStatus secStatus = SECFailure;

  if (! certFile)
    {
      nsscommon_error (_F("Could not open certificate file %s for reading\n%s",
			  certPath.c_str (), strerror (errno)));
      return SECFailure;
    }

  int fd = fileno (certFile);
  struct stat info;
  int rc = fstat (fd, &info);
  if (rc != 0)
    {
      nsscommon_error (_F("Could not obtain information about certificate file %s\n%s",
			  certPath.c_str (), strerror (errno)));
      fclose (certFile);
      return SECFailure;
    }

  PLArenaPool *arena = PORT_NewArena(DER_DEFAULT_CHUNKSIZE);
  if (!arena)
    {
      nsscommon_error (_F("Could not create arena while decoding certificate from file %s",
			  certPath.c_str ()));
      fclose (certFile);
      goto done;
    }
      
  SECItem derCert;
  if (!SECITEM_AllocItem(arena, &derCert, info.st_size))
    {
      nsscommon_error (_F("Could not allocate DER cert\n%s",
			  strerror (errno)));
      fclose (certFile);
      goto done;
    }

  size_t read;
  read = fread (derCert.data, 1, derCert.len, certFile);
  fclose (certFile);
  if (read != derCert.len)
    {
      nsscommon_error (_F("Error reading from certificate file %s\n%s",
			  certPath.c_str (), strerror (errno)));
      goto done;
    }
  derCert.type = siDERCertBuffer;

  // Sigh. We'd like to use CERT_DecodeDERCertificate() here, but
  // although /usr/include/nss3/cert.h declares them, the shared
  // library doesn't export them.

  CERTCertificate *cert;
  int rv;
  char *str;

  // Strip off the signature.
  CERTSignedData *sd;
  sd = PORT_ArenaZNew(arena, CERTSignedData);
  if (!sd)
    {
      nsscommon_error (_F("Could not allocate signed data while decoding certificate from file %s",
			  certPath.c_str ()));
      goto done;
    }
  rv = SEC_ASN1DecodeItem(arena, sd, SEC_ASN1_GET(CERT_SignedDataTemplate), 
			  &derCert);
  if (rv)
    {
      nsscommon_error (_F("Could not decode signature while decoding certificate from file %s",
			  certPath.c_str ()));
      goto done;
    }

  // Decode the certificate.
  cert = PORT_ArenaZNew(arena, CERTCertificate);
  if (!cert)
    {
      nsscommon_error (_F("Could not allocate cert while decoding certificate from file %s",
			  certPath.c_str ()));
      goto done;
    }
  cert->arena = arena;
  rv = SEC_ASN1DecodeItem(arena, cert,
			  SEC_ASN1_GET(CERT_CertificateTemplate), &sd->data);
  if (rv)
    {
      nsscommon_error (_F("Could not decode certificate from file %s",
			  certPath.c_str ()));
      goto done;
    }

  // Get the fingerprint from the signature.
  unsigned char fingerprint_buf[SHA1_LENGTH];
  SECItem fpItem;
  rv = PK11_HashBuf(SEC_OID_SHA1, fingerprint_buf, derCert.data, derCert.len);
  if (rv)
    {
      nsscommon_error (_F("Could not decode SHA1 fingerprint from file %s",
			  certPath.c_str ()));
      goto done;
    }
  fpItem.data = fingerprint_buf;
  fpItem.len = sizeof(fingerprint_buf);
  str = CERT_Hexify(&fpItem, 1);
  if (! str)
  {
      nsscommon_error (_F("Could not hexify SHA1 fingerprint from file %s",
			  certPath.c_str ()));
      goto done;
  }
  fingerprint = str;
  transform(fingerprint.begin(), fingerprint.end(), fingerprint.begin(), 
	    ::tolower);
  PORT_Free(str);
  secStatus = SECSuccess;

done:
  if (arena)
    PORT_FreeArena(arena, PR_FALSE);
  return secStatus;
}

/* vim: set sw=2 ts=8 cino=>4,n-2,{2,^-2,t0,(0,u0,w1,M1 : */
