//========================================================================
//
// SignatureHandler.cc
//
// This file is licensed under the GPLv2 or later
//
// Copyright 2015, 2016 André Guerreiro <aguerreiro1985@gmail.com>
// Copyright 2015 André Esser <bepandre@hotmail.com>
// Copyright 2015, 2016, 2018, 2019 Albert Astals Cid <aacid@kde.org>
// Copyright 2015 Markus Kilås <digital@markuspage.com>
// Copyright 2017 Sebastian Rasmussen <sebras@gmail.com>
// Copyright 2017 Hans-Ulrich Jüttner <huj@froreich-bioscientia.de>
// Copyright 2018 Chinmoy Ranjan Pradhan <chinmoyrp65@protonmail.com>
// Copyright 2018 Oliver Sander <oliver.sander@tu-dresden.de>
//
//========================================================================

#include <config.h>

#include "SignatureHandler.h"
#include "goo/gmem.h"
#include <secmod.h>
#include <keyhi.h>
#include <secder.h>

#include <dirent.h>
#include <Error.h>

static void shutdownNss()
{
  if (NSS_Shutdown() != SECSuccess) {
    fprintf(stderr, "NSS_Shutdown failed: %s\n", PR_ErrorToString(PORT_GetError(), PR_LANGUAGE_I_DEFAULT));
  }
}

unsigned int SignatureHandler::digestLength(SECOidTag digestAlgId)
{
  switch(digestAlgId){
    case SEC_OID_SHA1:
      return 20;
    case SEC_OID_SHA256:
      return 32;
    case SEC_OID_SHA384:
      return 48;
    case SEC_OID_SHA512:
      return 64;
    default:
      printf("ERROR: Unrecognized Hash ID\n");
      return 0;
  }
}

char *SignatureHandler::getSignerName()
{
  if (!CMSSignerInfo || !NSS_IsInitialized())
      return nullptr;

  CERTCertificate *cert = NSS_CMSSignerInfo_GetSigningCertificate(CMSSignerInfo, CERT_GetDefaultCertDB());

  if (!cert)
      return nullptr;

  return CERT_GetCommonName(&cert->subject);
}

const char * SignatureHandler::getSignerSubjectDN()
{
  if (!CMSSignerInfo)
    return nullptr;

  CERTCertificate *cert = NSS_CMSSignerInfo_GetSigningCertificate(CMSSignerInfo, CERT_GetDefaultCertDB());
  if (!cert)
    return nullptr;
  return cert->subjectName;
}

HASH_HashType SignatureHandler::getHashAlgorithm()
{
  if (hash_context && hash_context->hashobj)
  {
    return hash_context->hashobj->type;
  }
  return HASH_AlgNULL;
}

time_t SignatureHandler::getSigningTime()
{
  PRTime sTime; // time in microseconds since the epoch

  if (NSS_CMSSignerInfo_GetSigningTime (CMSSignerInfo, &sTime) != SECSuccess)
    return 0;

  return static_cast<time_t>(sTime/1000000);
}

X509CertificateInfo::EntityInfo SignatureHandler::getEntityInfo(CERTName *entityName) const
{
  X509CertificateInfo::EntityInfo info;

  if (!entityName)
    return info;

  char *dn = CERT_NameToAscii(entityName);
  if (dn) {
      info.distinguishedName = dn;
      PORT_Free(dn);
  }

  char *cn = CERT_GetCommonName(entityName);
  if (cn) {
    info.commonName = cn;
    PORT_Free(cn);
  }

  char *email = CERT_GetCertEmailAddress(entityName);
  if (email) {
    info.email = email;
    PORT_Free(email);
  }

  char *org = CERT_GetOrgName(entityName);
  if (org) {
    info.organization = org;
    PORT_Free(org);
  }

  return info;
}

static GooString SECItemToGooString(const SECItem &secItem)
{
  // TODO do we need to handle secItem.type;
  return GooString((const char *)secItem.data, secItem.len);
}

std::unique_ptr<X509CertificateInfo> SignatureHandler::getCertificateInfo() const
{
  if (!CMSSignerInfo)
    return nullptr;

  CERTCertificate *cert = NSS_CMSSignerInfo_GetSigningCertificate(CMSSignerInfo, CERT_GetDefaultCertDB());
  if (!cert)
    return nullptr;

  auto certInfo = std::make_unique<X509CertificateInfo>();

  certInfo->setVersion(DER_GetInteger(&cert->version) + 1);
  certInfo->setSerialNumber(SECItemToGooString(cert->serialNumber));

  //issuer info
  certInfo->setIssuerInfo(getEntityInfo(&cert->issuer));

  //validity
  PRTime notBefore, notAfter;
  CERT_GetCertTimes(cert, &notBefore, &notAfter);
  X509CertificateInfo::Validity certValidity;
  certValidity.notBefore = static_cast<time_t>(notBefore/1000000);
  certValidity.notAfter = static_cast<time_t>(notAfter/1000000);
  certInfo->setValidity(certValidity);

  //subject info
  certInfo->setSubjectInfo(getEntityInfo(&cert->subject));

  //public key info
  X509CertificateInfo::PublicKeyInfo pkInfo;
  SECKEYPublicKey *pk = CERT_ExtractPublicKey(cert);
  switch (pk->keyType)
  {
    case rsaKey:
      pkInfo.publicKey = SECItemToGooString(pk->u.rsa.modulus);
      pkInfo.publicKeyType = RSAKEY;
      break;
    case dsaKey:
      pkInfo.publicKey = SECItemToGooString(pk->u.dsa.publicValue);
      pkInfo.publicKeyType = DSAKEY;
      break;
    case ecKey:
      pkInfo.publicKey = SECItemToGooString(pk->u.ec.publicValue);
      pkInfo.publicKeyType = ECKEY;
      break;
    default:
      pkInfo.publicKey = SECItemToGooString(cert->subjectPublicKeyInfo.subjectPublicKey);
      pkInfo.publicKeyType = OTHERKEY;
      break;
  }
  pkInfo.publicKeyStrength = SECKEY_PublicKeyStrengthInBits(pk);
  certInfo->setPublicKeyInfo(std::move(pkInfo));

  certInfo->setKeyUsageExtensions(cert->keyUsage);
  certInfo->setCertificateDER(SECItemToGooString(cert->derCert));
  certInfo->setIsSelfSigned(CERT_CompareName(&cert->subject, &cert->issuer) == SECEqual);

  SECKEY_DestroyPublicKey(pk);

  return certInfo;
}

static GooString *getDefaultFirefoxCertDB_Linux()
{
  GooString * finalPath = nullptr;
  DIR *toSearchIn;
  struct dirent *subFolder;

  GooString * homePath = new GooString(getenv("HOME"));
  homePath = homePath->append("/.mozilla/firefox/");

  if ((toSearchIn = opendir(homePath->c_str())) == nullptr) {
    error(errInternal, 0, "couldn't find default Firefox Folder");
    delete homePath;
    return nullptr;
  }
  do {
    if ((subFolder = readdir(toSearchIn)) != nullptr) {
      if (strstr(subFolder->d_name, "default") != nullptr) {
        finalPath = homePath->append(subFolder->d_name);
        closedir(toSearchIn);
        return finalPath;
      }
    }
  } while (subFolder != nullptr);

  closedir(toSearchIn);
  delete homePath;
  return nullptr;
}

/**
 * Initialise NSS
 */
void SignatureHandler::setNSSDir(const GooString &nssDir)
{
  static bool setNssDirCalled = false;

  if (NSS_IsInitialized() && nssDir.getLength() > 0) {
    error(errInternal, 0, "You need to call setNSSDir before signature validation related operations happen");
    return;
  }

  if (setNssDirCalled)
    return;

  setNssDirCalled = true;

  atexit(shutdownNss);

  bool initSuccess = false;
  if (nssDir.getLength() > 0) {
    initSuccess = (NSS_Init(nssDir.c_str()) == SECSuccess);
  } else {
    GooString *certDBPath = getDefaultFirefoxCertDB_Linux();
    if (certDBPath == nullptr) {
      initSuccess = (NSS_Init("sql:/etc/pki/nssdb") == SECSuccess);
    } else {
      initSuccess = (NSS_Init(certDBPath->c_str()) == SECSuccess);
    }
    if (!initSuccess) {
      GooString homeNssDb(getenv("HOME"));
      homeNssDb.append("/.pki/nssdb");
      initSuccess = (NSS_Init(homeNssDb.c_str()) == SECSuccess);
      if (!initSuccess) {
	NSS_NoDB_Init(nullptr);
      }
    }
    delete certDBPath;
  }

  if (initSuccess) {
    //Make sure NSS root certificates module is loaded
    SECMOD_AddNewModule("Root Certs", "libnssckbi.so", 0, 0);
  }
}


SignatureHandler::SignatureHandler(unsigned char *p7, int p7_length)
 : hash_context(nullptr),
   CMSMessage(nullptr),
   CMSSignedData(nullptr),
   CMSSignerInfo(nullptr),
   temp_certs(nullptr)
{
  setNSSDir({});
  CMSitem.data = p7;
  CMSitem.len = p7_length;
  CMSMessage = CMS_MessageCreate(&CMSitem);
  CMSSignedData = CMS_SignedDataCreate(CMSMessage);
  if (CMSSignedData) {
    CMSSignerInfo = CMS_SignerInfoCreate(CMSSignedData);
    hash_context = initHashContext();
  }
}

HASHContext * SignatureHandler::initHashContext()
{

  SECItem usedAlgorithm = NSS_CMSSignedData_GetDigestAlgs(CMSSignedData)[0]->algorithm;
  hash_length = digestLength(SECOID_FindOIDTag(&usedAlgorithm));
  HASH_HashType hashType;
  hashType    = HASH_GetHashTypeByOidTag(SECOID_FindOIDTag(&usedAlgorithm));
  return HASH_Create(hashType);
}

void SignatureHandler::updateHash(unsigned char * data_block, int data_len)
{
  if (hash_context) {
    HASH_Update(hash_context, data_block, data_len);
  }
}

SignatureHandler::~SignatureHandler()
{
  SECITEM_FreeItem(&CMSitem, PR_FALSE);
  if (CMSMessage)
    NSS_CMSMessage_Destroy(CMSMessage);

  if (hash_context)
    HASH_Destroy(hash_context);

  free(temp_certs);
}

NSSCMSMessage *SignatureHandler::CMS_MessageCreate(SECItem * cms_item)
{
  if (cms_item->data){
    return NSS_CMSMessage_CreateFromDER(cms_item, nullptr, nullptr /* Content callback */
                        , nullptr, nullptr /*Password callback*/
                        , nullptr, nullptr /*Decrypt callback*/);
  } else {
    return nullptr;
  }
}

NSSCMSSignedData *SignatureHandler::CMS_SignedDataCreate(NSSCMSMessage * cms_msg)
{
  if (!NSS_CMSMessage_IsSigned(cms_msg)) {
    error(errInternal, 0, "Input couldn't be parsed as a CMS signature");
    return nullptr;
  }

  NSSCMSContentInfo *cinfo = NSS_CMSMessage_ContentLevel(cms_msg, 0);
  if (!cinfo) {
    error(errInternal, 0, "Error in NSS_CMSMessage_ContentLevel");
    return nullptr;
  }

  NSSCMSSignedData *signedData = (NSSCMSSignedData*) NSS_CMSContentInfo_GetContent(cinfo);
  if (!signedData) {
    error(errInternal, 0, "CError in NSS_CMSContentInfo_GetContent()");
    return nullptr;
  }

  if (signedData->rawCerts)
  {
    size_t i;
    for (i = 0; signedData->rawCerts[i]; ++i) {} // just count the length of the certificate chain

    // tempCerts field needs to be filled for complete memory release by NSSCMSSignedData_Destroy
    signedData->tempCerts = (CERTCertificate **) gmallocn( i+1, sizeof(CERTCertificate *));
    memset(signedData->tempCerts, 0, (i+1) * sizeof(CERTCertificate *));
    // store the addresses of these temporary certificates for future release
    for (i = 0; signedData->rawCerts[i]; ++i)
      signedData->tempCerts[i] = CERT_NewTempCertificate(CERT_GetDefaultCertDB(), signedData->rawCerts[i], nullptr, 0, 0);

    temp_certs = signedData->tempCerts;
    return signedData;
  } else {
    return nullptr;
  }
}

NSSCMSSignerInfo *SignatureHandler::CMS_SignerInfoCreate(NSSCMSSignedData * cms_sig_data)
{
  NSSCMSSignerInfo *signerInfo = NSS_CMSSignedData_GetSignerInfo(cms_sig_data, 0);
  if (!signerInfo) {
    printf("Error in NSS_CMSSignedData_GetSignerInfo()\n");
    return nullptr;
  } else {
    return signerInfo;
  }
}

static SignatureValidationStatus NSS_SigTranslate(NSSCMSVerificationStatus nss_code)
{
  switch(nss_code)
  {
    case NSSCMSVS_GoodSignature:
      return SIGNATURE_VALID;

    case NSSCMSVS_BadSignature:
      return SIGNATURE_INVALID;

    case NSSCMSVS_DigestMismatch:
      return SIGNATURE_DIGEST_MISMATCH;

    case NSSCMSVS_ProcessingError:
      return SIGNATURE_DECODING_ERROR;

    default:
      return SIGNATURE_GENERIC_ERROR;
  }
}

SignatureValidationStatus SignatureHandler::validateSignature()
{
  unsigned char *digest_buffer = nullptr;

  if (!CMSSignedData)
    return SIGNATURE_GENERIC_ERROR;

  if (!NSS_IsInitialized())
    return SIGNATURE_GENERIC_ERROR;

  if (!hash_context)
    return SIGNATURE_GENERIC_ERROR;

  digest_buffer = (unsigned char *)PORT_Alloc(hash_length);
  unsigned int result_len = 0;

  HASH_End(hash_context, digest_buffer, &result_len, hash_length);

  SECItem digest;
  digest.data = digest_buffer;
  digest.len = hash_length;

  if ((NSS_CMSSignerInfo_GetSigningCertificate(CMSSignerInfo, CERT_GetDefaultCertDB())) == nullptr)
    CMSSignerInfo->verificationStatus = NSSCMSVS_SigningCertNotFound;

  SECItem * content_info_data = CMSSignedData->contentInfo.content.data;
  if (content_info_data != nullptr && content_info_data->data != nullptr)
  {
    /*
      This means it's not a detached type signature
      so the digest is contained in SignedData->contentInfo
    */
    if (memcmp(digest.data, content_info_data->data, hash_length) == 0
        && digest.len == content_info_data->len)
    {
      PORT_Free(digest_buffer);
      return SIGNATURE_VALID;
    }
    else
    {
      PORT_Free(digest_buffer);
      return SIGNATURE_DIGEST_MISMATCH;
    }

  }
  else if (NSS_CMSSignerInfo_Verify(CMSSignerInfo, &digest, nullptr) != SECSuccess)
  {

    PORT_Free(digest_buffer);
    return NSS_SigTranslate(CMSSignerInfo->verificationStatus);
  }
  else
  {
    PORT_Free(digest_buffer);
    return SIGNATURE_VALID;
  }
}

CertificateValidationStatus SignatureHandler::validateCertificate(time_t validation_time)
{
  CERTCertificate *cert;

  if (!CMSSignerInfo)
    return CERTIFICATE_GENERIC_ERROR;

  if ((cert = NSS_CMSSignerInfo_GetSigningCertificate(CMSSignerInfo, CERT_GetDefaultCertDB())) == nullptr)
    CMSSignerInfo->verificationStatus = NSSCMSVS_SigningCertNotFound;

  PRTime vTime = 0; // time in microseconds since the epoch, special value 0 means now
  if (validation_time > 0)
    vTime = 1000000*(PRTime)validation_time;
  CERTValInParam inParams[3];
  inParams[0].type = cert_pi_revocationFlags;
  inParams[0].value.pointer.revocation = CERT_GetClassicOCSPEnabledSoftFailurePolicy();
  inParams[1].type = cert_pi_date;
  inParams[1].value.scalar.time = vTime;
  inParams[2].type = cert_pi_end;

  CERT_PKIXVerifyCert(cert, certificateUsageEmailSigner, inParams, nullptr,
                CMSSignerInfo->cmsg->pwfn_arg);

  switch(PORT_GetError())
  {
    // 0 not defined in SECErrorCodes, it means success for this purpose.
    case 0:
      return CERTIFICATE_TRUSTED;

    case SEC_ERROR_UNKNOWN_ISSUER:
      return CERTIFICATE_UNKNOWN_ISSUER;

    case SEC_ERROR_UNTRUSTED_ISSUER:
      return CERTIFICATE_UNTRUSTED_ISSUER;

    case SEC_ERROR_REVOKED_CERTIFICATE:
      return CERTIFICATE_REVOKED;

    case SEC_ERROR_EXPIRED_CERTIFICATE:
      return CERTIFICATE_EXPIRED;
  }

  return CERTIFICATE_GENERIC_ERROR;
}
