// Copyright 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "chromeos/network/onc/onc_certificate_importer_impl.h"

#include <cert.h>
#include <keyhi.h>
#include <pk11pub.h>
#include <stddef.h>

#include "base/base64.h"
#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/callback.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/sequenced_task_runner.h"
#include "base/single_thread_task_runner.h"
#include "base/task_runner_util.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "base/threading/thread_task_runner_handle.h"
#include "base/values.h"
#include "chromeos/network/network_event_log.h"
#include "chromeos/network/onc/onc_parsed_certificates.h"
#include "chromeos/network/onc/onc_utils.h"
#include "crypto/scoped_nss_types.h"
#include "net/base/net_errors.h"
#include "net/cert/nss_cert_database.h"
#include "net/cert/x509_util_nss.h"

namespace chromeos {
namespace onc {

CertificateImporterImpl::CertificateImporterImpl(
    const scoped_refptr<base::SequencedTaskRunner>& io_task_runner,
    net::NSSCertDatabase* target_nssdb)
    : io_task_runner_(io_task_runner),
      target_nssdb_(target_nssdb),
      weak_factory_(this) {
  CHECK(target_nssdb);
}

CertificateImporterImpl::~CertificateImporterImpl() = default;

void CertificateImporterImpl::ImportAllCertificatesUserInitiated(
    const std::vector<OncParsedCertificates::ServerOrAuthorityCertificate>&
        server_or_authority_certificates,
    const std::vector<OncParsedCertificates::ClientCertificate>&
        client_certificates,
    DoneCallback done_callback) {
  VLOG(2) << "Importing " << server_or_authority_certificates.size()
          << " server/authority certificates and " << client_certificates.size()
          << " client certificates";
  RunTaskOnIOTaskRunnerAndCallDoneCallback(
      base::BindOnce(&StoreAllCertificatesUserInitiated,
                     server_or_authority_certificates, client_certificates,
                     target_nssdb_),
      std::move(done_callback));
}

void CertificateImporterImpl::ImportClientCertificates(
    const std::vector<OncParsedCertificates::ClientCertificate>&
        client_certificates,
    DoneCallback done_callback) {
  VLOG(2) << "Permanently importing " << client_certificates.size()
          << " client certificates";
  RunTaskOnIOTaskRunnerAndCallDoneCallback(
      base::BindOnce(&StoreClientCertificates, client_certificates,
                     target_nssdb_),
      std::move(done_callback));
}

void CertificateImporterImpl::RunTaskOnIOTaskRunnerAndCallDoneCallback(
    base::OnceCallback<bool()> task,
    DoneCallback done_callback) {
  // Thereforce, call back to |this|. This check of |this| must happen last and
  // on the origin thread.
  DoneCallback callback_to_this =
      base::BindOnce(&CertificateImporterImpl::RunDoneCallback,
                     weak_factory_.GetWeakPtr(), std::move(done_callback));

  // The NSSCertDatabase must be accessed on |io_task_runner_|
  base::PostTaskAndReplyWithResult(io_task_runner_.get(), FROM_HERE,
                                   std::move(task),
                                   std::move(callback_to_this));
}

void CertificateImporterImpl::RunDoneCallback(DoneCallback callback,
                                              bool success) {
  if (!success)
    NET_LOG_ERROR("ONC Certificate Import Error", "");
  std::move(callback).Run(success);
}

// static
bool CertificateImporterImpl::StoreClientCertificates(
    const std::vector<OncParsedCertificates::ClientCertificate>&
        client_certificates,
    net::NSSCertDatabase* nssdb) {
  bool success = true;

  for (const OncParsedCertificates::ClientCertificate& client_certificate :
       client_certificates) {
    if (!StoreClientCertificate(client_certificate, nssdb)) {
      success = false;
    } else {
      VLOG(2) << "Successfully imported certificate with GUID "
              << client_certificate.guid();
    }
  }
  return success;
}

// static
bool CertificateImporterImpl::StoreAllCertificatesUserInitiated(
    const std::vector<OncParsedCertificates::ServerOrAuthorityCertificate>&
        server_or_authority_certificates,
    const std::vector<OncParsedCertificates::ClientCertificate>&
        client_certificates,
    net::NSSCertDatabase* nssdb) {
  bool success = true;

  for (const OncParsedCertificates::ServerOrAuthorityCertificate&
           server_or_authority_cert : server_or_authority_certificates) {
    if (!StoreServerOrCaCertificateUserInitiated(server_or_authority_cert,
                                                 nssdb)) {
      success = false;
    } else {
      VLOG(2) << "Successfully imported certificate with GUID "
              << server_or_authority_cert.guid();
    }
  }
  if (!StoreClientCertificates(client_certificates, nssdb))
    success = false;

  return success;
}

// static
bool CertificateImporterImpl::StoreServerOrCaCertificateUserInitiated(
    const OncParsedCertificates::ServerOrAuthorityCertificate& certificate,
    net::NSSCertDatabase* nssdb) {
  net::ScopedCERTCertificate x509_cert =
      net::x509_util::CreateCERTCertificateFromX509Certificate(
          certificate.certificate().get());
  if (!x509_cert.get()) {
    LOG(ERROR) << "Unable to create certificate: " << certificate.guid();
    return false;
  }

  // Permanent web trust is granted to certificates imported by the user - and
  // StoreServerOrCaCertificateUserInitiated is only used if the user initiated
  // the import.
  net::NSSCertDatabase::TrustBits trust =
      (certificate.web_trust_requested() ? net::NSSCertDatabase::TRUSTED_SSL
                                         : net::NSSCertDatabase::TRUST_DEFAULT);

  if (x509_cert.get()->isperm) {
    net::CertType net_cert_type =
        certificate.type() == OncParsedCertificates::
                                  ServerOrAuthorityCertificate::Type::kServer
            ? net::SERVER_CERT
            : net::CA_CERT;
    VLOG(1) << "Certificate is already installed.";
    net::NSSCertDatabase::TrustBits missing_trust_bits =
        trust & ~nssdb->GetCertTrust(x509_cert.get(), net_cert_type);
    if (missing_trust_bits) {
      std::string error_reason;
      bool success = false;
      if (nssdb->IsReadOnly(x509_cert.get())) {
        error_reason = " Certificate is stored read-only.";
      } else {
        success = nssdb->SetCertTrust(x509_cert.get(), net_cert_type, trust);
      }
      if (!success) {
        LOG(ERROR) << "Certificate " << certificate.guid()
                   << " was already present, but trust couldn't be set."
                   << error_reason;
      }
    }
  } else {
    net::ScopedCERTCertificateList cert_list;
    cert_list.push_back(net::x509_util::DupCERTCertificate(x509_cert.get()));
    net::NSSCertDatabase::ImportCertFailureList failures;
    bool success = false;
    if (certificate.type() ==
        OncParsedCertificates::ServerOrAuthorityCertificate::Type::kServer)
      success = nssdb->ImportServerCert(cert_list, trust, &failures);
    else  // Authority cert
      success = nssdb->ImportCACerts(cert_list, trust, &failures);

    if (!failures.empty()) {
      std::string error_string = net::ErrorToString(failures[0].net_error);
      LOG(ERROR) << "Error ( " << error_string << " ) importing certificate "
                 << certificate.guid();
      return false;
    }

    if (!success) {
      LOG(ERROR) << "Unknown error importing certificate "
                 << certificate.guid();
      return false;
    }
  }

  return true;
}

// static
bool CertificateImporterImpl::StoreClientCertificate(
    const OncParsedCertificates::ClientCertificate& certificate,
    net::NSSCertDatabase* nssdb) {
  // Since this has a private key, always use the private module.
  crypto::ScopedPK11Slot private_slot(nssdb->GetPrivateSlot());
  if (!private_slot)
    return false;

  net::ScopedCERTCertificateList imported_certs;

  int import_result =
      nssdb->ImportFromPKCS12(private_slot.get(), certificate.pkcs12_data(),
                              base::string16(), false, &imported_certs);
  if (import_result != net::OK) {
    std::string error_string = net::ErrorToString(import_result);
    LOG(ERROR) << "Unable to import client certificate with guid "
               << certificate.guid() << ", error: " << error_string;
    return false;
  }

  if (imported_certs.size() == 0) {
    LOG(WARNING) << "PKCS12 data contains no importable certificates for guid "
                 << certificate.guid();
    return true;
  }

  if (imported_certs.size() != 1) {
    LOG(WARNING) << "PKCS12 data for guid " << certificate.guid()
                 << " contains more than one certificate. "
                    "Only the first one will be imported.";
  }

  CERTCertificate* cert_result = imported_certs[0].get();

  // Find the private key associated with this certificate, and set the
  // nickname on it.
  SECKEYPrivateKey* private_key = PK11_FindPrivateKeyFromCert(
      cert_result->slot, cert_result, nullptr /* wincx */);
  if (private_key) {
    PK11_SetPrivateKeyNickname(private_key,
                               const_cast<char*>(certificate.guid().c_str()));
    SECKEY_DestroyPrivateKey(private_key);
  } else {
    LOG(WARNING) << "Unable to find private key for certificate "
                 << certificate.guid();
  }
  return true;
}

}  // namespace onc
}  // namespace chromeos
