/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 et sw=2 tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "ContentSignatureVerifier.h"

#include "BRNameMatchingPolicy.h"
#include "SharedCertVerifier.h"
#include "cryptohi.h"
#include "keyhi.h"
#include "mozilla/Assertions.h"
#include "mozilla/Casting.h"
#include "mozilla/Unused.h"
#include "nsCOMPtr.h"
#include "nsContentUtils.h"
#include "nsISupportsPriority.h"
#include "nsIURI.h"
#include "nsNSSComponent.h"
#include "nsSecurityHeaderParser.h"
#include "nsStreamUtils.h"
#include "nsWhitespaceTokenizer.h"
#include "nsXPCOMStrings.h"
#include "nssb64.h"
#include "pkix/pkix.h"
#include "pkix/pkixtypes.h"
#include "secerr.h"

NS_IMPL_ISUPPORTS(ContentSignatureVerifier,
                  nsIContentSignatureVerifier,
                  nsIInterfaceRequestor,
                  nsIStreamListener)

using namespace mozilla;
using namespace mozilla::pkix;
using namespace mozilla::psm;

static LazyLogModule gCSVerifierPRLog("ContentSignatureVerifier");
#define CSVerifier_LOG(args) MOZ_LOG(gCSVerifierPRLog, LogLevel::Debug, args)

// Content-Signature prefix
const nsLiteralCString kPREFIX = NS_LITERAL_CSTRING("Content-Signature:\x00");

ContentSignatureVerifier::~ContentSignatureVerifier()
{
  nsNSSShutDownPreventionLock locker;
  if (isAlreadyShutDown()) {
    return;
  }
  destructorSafeDestroyNSSReference();
  shutdown(ShutdownCalledFrom::Object);
}

NS_IMETHODIMP
ContentSignatureVerifier::VerifyContentSignature(
  const nsACString& aData, const nsACString& aCSHeader,
  const nsACString& aCertChain, const nsACString& aName, bool* _retval)
{
  NS_ENSURE_ARG(_retval);
  nsresult rv = CreateContext(aData, aCSHeader, aCertChain, aName);
  if (NS_FAILED(rv)) {
    *_retval = false;
    CSVerifier_LOG(("CSVerifier: Signature verification failed\n"));
    if (rv == NS_ERROR_INVALID_SIGNATURE) {
      return NS_OK;
    }
    return rv;
  }

  return End(_retval);
}

bool
IsNewLine(char16_t c)
{
  return c == '\n' || c == '\r';
}

nsresult
ReadChainIntoCertList(const nsACString& aCertChain, CERTCertList* aCertList,
                      const nsNSSShutDownPreventionLock& /*proofOfLock*/)
{
  bool inBlock = false;
  bool certFound = false;

  const nsCString header = NS_LITERAL_CSTRING("-----BEGIN CERTIFICATE-----");
  const nsCString footer = NS_LITERAL_CSTRING("-----END CERTIFICATE-----");

  nsCWhitespaceTokenizerTemplate<IsNewLine> tokenizer(aCertChain);

  nsAutoCString blockData;
  while (tokenizer.hasMoreTokens()) {
    nsDependentCSubstring token = tokenizer.nextToken();
    if (token.IsEmpty()) {
      continue;
    }
    if (inBlock) {
      if (token.Equals(footer)) {
        inBlock = false;
        certFound = true;
        // base64 decode data, make certs, append to chain
        ScopedAutoSECItem der;
        if (!NSSBase64_DecodeBuffer(nullptr, &der, blockData.BeginReading(),
                                    blockData.Length())) {
          CSVerifier_LOG(("CSVerifier: decoding the signature failed\n"));
          return NS_ERROR_FAILURE;
        }
        UniqueCERTCertificate tmpCert(
          CERT_NewTempCertificate(CERT_GetDefaultCertDB(), &der, nullptr, false,
                                  true));
        if (!tmpCert) {
          return NS_ERROR_FAILURE;
        }
        // if adding tmpCert succeeds, tmpCert will now be owned by aCertList
        SECStatus res = CERT_AddCertToListTail(aCertList, tmpCert.get());
        if (res != SECSuccess) {
          return MapSECStatus(res);
        }
        Unused << tmpCert.release();
      } else {
        blockData.Append(token);
      }
    } else if (token.Equals(header)) {
      inBlock = true;
      blockData = "";
    }
  }
  if (inBlock || !certFound) {
    // the PEM data did not end; bad data.
    CSVerifier_LOG(("CSVerifier: supplied chain contains bad data\n"));
    return NS_ERROR_FAILURE;
  }
  return NS_OK;
}

nsresult
ContentSignatureVerifier::CreateContextInternal(const nsACString& aData,
                                                const nsACString& aCertChain,
                                                const nsACString& aName)
{
  MOZ_ASSERT(NS_IsMainThread());
  nsNSSShutDownPreventionLock locker;
  if (isAlreadyShutDown()) {
    CSVerifier_LOG(("CSVerifier: nss is already shutdown\n"));
    return NS_ERROR_FAILURE;
  }

  UniqueCERTCertList certCertList(CERT_NewCertList());
  if (!certCertList) {
    return NS_ERROR_OUT_OF_MEMORY;
  }

  nsresult rv = ReadChainIntoCertList(aCertChain, certCertList.get(), locker);
  if (NS_FAILED(rv)) {
    return rv;
  }

  CERTCertListNode* node = CERT_LIST_HEAD(certCertList.get());
  if (!node || CERT_LIST_END(node, certCertList.get()) || !node->cert) {
    return NS_ERROR_FAILURE;
  }

  SECItem* certSecItem = &node->cert->derCert;

  Input certDER;
  mozilla::pkix::Result result =
    certDER.Init(BitwiseCast<uint8_t*, unsigned char*>(certSecItem->data),
                 certSecItem->len);
  if (result != Success) {
    return NS_ERROR_FAILURE;
  }


  // Check the signerCert chain is good
  CSTrustDomain trustDomain(certCertList);
  result = BuildCertChain(trustDomain, certDER, Now(),
                          EndEntityOrCA::MustBeEndEntity,
                          KeyUsage::noParticularKeyUsageRequired,
                          KeyPurposeId::id_kp_codeSigning,
                          CertPolicyId::anyPolicy,
                          nullptr/*stapledOCSPResponse*/);
  if (result != Success) {
    // if there was a library error, return an appropriate error
    if (IsFatalError(result)) {
      return NS_ERROR_FAILURE;
    }
    // otherwise, assume the signature was invalid
    CSVerifier_LOG(("CSVerifier: The supplied chain is bad\n"));
    return NS_ERROR_INVALID_SIGNATURE;
  }

  // Check the SAN
  Input hostnameInput;

  result = hostnameInput.Init(
    BitwiseCast<const uint8_t*, const char*>(aName.BeginReading()),
    aName.Length());
  if (result != Success) {
    return NS_ERROR_FAILURE;
  }

  BRNameMatchingPolicy nameMatchingPolicy(BRNameMatchingPolicy::Mode::Enforce);
  result = CheckCertHostname(certDER, hostnameInput, nameMatchingPolicy);
  if (result != Success) {
    return NS_ERROR_INVALID_SIGNATURE;
  }

  mKey.reset(CERT_ExtractPublicKey(node->cert));

  // in case we were not able to extract a key
  if (!mKey) {
    CSVerifier_LOG(("CSVerifier: unable to extract a key\n"));
    return NS_ERROR_INVALID_SIGNATURE;
  }

  // Base 64 decode the signature
  ScopedAutoSECItem rawSignatureItem;
  if (!NSSBase64_DecodeBuffer(nullptr, &rawSignatureItem, mSignature.get(),
                              mSignature.Length())) {
    CSVerifier_LOG(("CSVerifier: decoding the signature failed\n"));
    return NS_ERROR_FAILURE;
  }

  // get signature object
  ScopedAutoSECItem signatureItem;
  // We have a raw ecdsa signature r||s so we have to DER-encode it first
  // Note that we have to check rawSignatureItem->len % 2 here as
  // DSAU_EncodeDerSigWithLen asserts this
  if (rawSignatureItem.len == 0 || rawSignatureItem.len % 2 != 0) {
    CSVerifier_LOG(("CSVerifier: signature length is bad\n"));
    return NS_ERROR_FAILURE;
  }
  if (DSAU_EncodeDerSigWithLen(&signatureItem, &rawSignatureItem,
                               rawSignatureItem.len) != SECSuccess) {
    CSVerifier_LOG(("CSVerifier: encoding the signature failed\n"));
    return NS_ERROR_FAILURE;
  }

  // this is the only OID we support for now
  SECOidTag oid = SEC_OID_ANSIX962_ECDSA_SHA384_SIGNATURE;

  mCx = UniqueVFYContext(
    VFY_CreateContext(mKey.get(), &signatureItem, oid, nullptr));
  if (!mCx) {
    return NS_ERROR_INVALID_SIGNATURE;
  }

  if (VFY_Begin(mCx.get()) != SECSuccess) {
    return NS_ERROR_INVALID_SIGNATURE;
  }

  rv = UpdateInternal(kPREFIX, locker);
  if (NS_FAILED(rv)) {
    return rv;
  }
  // add data if we got any
  return UpdateInternal(aData, locker);
}

nsresult
ContentSignatureVerifier::DownloadCertChain()
{
  MOZ_ASSERT(NS_IsMainThread());

  if (mCertChainURL.IsEmpty()) {
    return NS_ERROR_INVALID_SIGNATURE;
  }

  nsCOMPtr<nsIURI> certChainURI;
  nsresult rv = NS_NewURI(getter_AddRefs(certChainURI), mCertChainURL);
  if (NS_FAILED(rv) || !certChainURI) {
    return rv;
  }

  // If the address is not https, fail.
  bool isHttps = false;
  rv = certChainURI->SchemeIs("https", &isHttps);
  if (NS_FAILED(rv)) {
    return rv;
  }
  if (!isHttps) {
    return NS_ERROR_INVALID_SIGNATURE;
  }

  rv = NS_NewChannel(getter_AddRefs(mChannel), certChainURI,
                     nsContentUtils::GetSystemPrincipal(),
                     nsILoadInfo::SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
                     nsIContentPolicy::TYPE_OTHER);
  if (NS_FAILED(rv)) {
    return rv;
  }

  // we need this chain soon
  nsCOMPtr<nsISupportsPriority> priorityChannel = do_QueryInterface(mChannel);
  if (priorityChannel) {
    priorityChannel->AdjustPriority(nsISupportsPriority::PRIORITY_HIGHEST);
  }

  rv = mChannel->AsyncOpen2(this);
  if (NS_FAILED(rv)) {
    return rv;
  }

  return NS_OK;
}

// Create a context for content signature verification using CreateContext below.
// This function doesn't require a cert chain to be passed, but instead aCSHeader
// must contain an x5u value that is then used to download the cert chain.
NS_IMETHODIMP
ContentSignatureVerifier::CreateContextWithoutCertChain(
  nsIContentSignatureReceiverCallback *aCallback, const nsACString& aCSHeader,
  const nsACString& aName)
{
  MOZ_ASSERT(NS_IsMainThread());
  MOZ_ASSERT(aCallback);
  if (mInitialised) {
    return NS_ERROR_ALREADY_INITIALIZED;
  }
  mInitialised = true;

  // we get the raw content-signature header here, so first parse aCSHeader
  nsresult rv = ParseContentSignatureHeader(aCSHeader);
  if (NS_FAILED(rv)) {
    return rv;
  }

  mCallback = aCallback;
  mName.Assign(aName);

  // We must download the cert chain now.
  // This is async and blocks createContextInternal calls.
  return DownloadCertChain();
}

// Create a context for a content signature verification.
// It sets signature, certificate chain and name that should be used to verify
// the data. The data parameter is the first part of the data to verify (this
// can be the empty string).
NS_IMETHODIMP
ContentSignatureVerifier::CreateContext(const nsACString& aData,
                                        const nsACString& aCSHeader,
                                        const nsACString& aCertChain,
                                        const nsACString& aName)
{
  if (mInitialised) {
    return NS_ERROR_ALREADY_INITIALIZED;
  }
  mInitialised = true;
  // The cert chain is given in aCertChain so we don't have to download anything.
  mHasCertChain = true;

  // we get the raw content-signature header here, so first parse aCSHeader
  nsresult rv = ParseContentSignatureHeader(aCSHeader);
  if (NS_FAILED(rv)) {
    return rv;
  }

  return CreateContextInternal(aData, aCertChain, aName);
}

nsresult
ContentSignatureVerifier::UpdateInternal(
  const nsACString& aData, const nsNSSShutDownPreventionLock& /*proofOfLock*/)
{
  if (!aData.IsEmpty()) {
    if (VFY_Update(mCx.get(), (const unsigned char*)nsPromiseFlatCString(aData).get(),
                   aData.Length()) != SECSuccess){
      return NS_ERROR_INVALID_SIGNATURE;
    }
  }
  return NS_OK;
}

/**
 * Add data to the context that shold be verified.
 */
NS_IMETHODIMP
ContentSignatureVerifier::Update(const nsACString& aData)
{
  MOZ_ASSERT(NS_IsMainThread());
  nsNSSShutDownPreventionLock locker;
  if (isAlreadyShutDown()) {
    CSVerifier_LOG(("CSVerifier: nss is already shutdown\n"));
    return NS_ERROR_FAILURE;
  }

  // If we didn't create the context yet, bail!
  if (!mHasCertChain) {
    MOZ_ASSERT_UNREACHABLE(
      "Someone called ContentSignatureVerifier::Update before "
      "downloading the cert chain.");
    return NS_ERROR_FAILURE;
  }

  return UpdateInternal(aData, locker);
}

/**
 * Finish signature verification and return the result in _retval.
 */
NS_IMETHODIMP
ContentSignatureVerifier::End(bool* _retval)
{
  NS_ENSURE_ARG(_retval);
  MOZ_ASSERT(NS_IsMainThread());
  nsNSSShutDownPreventionLock locker;
  if (isAlreadyShutDown()) {
    CSVerifier_LOG(("CSVerifier: nss is already shutdown\n"));
    return NS_ERROR_FAILURE;
  }

  // If we didn't create the context yet, bail!
  if (!mHasCertChain) {
    MOZ_ASSERT_UNREACHABLE(
      "Someone called ContentSignatureVerifier::End before "
      "downloading the cert chain.");
    return NS_ERROR_FAILURE;
  }

  *_retval = (VFY_End(mCx.get()) == SECSuccess);

  return NS_OK;
}

nsresult
ContentSignatureVerifier::ParseContentSignatureHeader(
  const nsACString& aContentSignatureHeader)
{
  MOZ_ASSERT(NS_IsMainThread());
  // We only support p384 ecdsa according to spec
  NS_NAMED_LITERAL_CSTRING(signature_var, "p384ecdsa");
  NS_NAMED_LITERAL_CSTRING(certChainURL_var, "x5u");

  nsSecurityHeaderParser parser(aContentSignatureHeader.BeginReading());
  nsresult rv = parser.Parse();
  if (NS_FAILED(rv)) {
    CSVerifier_LOG(("CSVerifier: could not parse ContentSignature header\n"));
    return NS_ERROR_FAILURE;
  }
  LinkedList<nsSecurityHeaderDirective>* directives = parser.GetDirectives();

  for (nsSecurityHeaderDirective* directive = directives->getFirst();
       directive != nullptr; directive = directive->getNext()) {
    CSVerifier_LOG(("CSVerifier: found directive %s\n", directive->mName.get()));
    if (directive->mName.Length() == signature_var.Length() &&
        directive->mName.EqualsIgnoreCase(signature_var.get(),
                                          signature_var.Length())) {
      if (!mSignature.IsEmpty()) {
        CSVerifier_LOG(("CSVerifier: found two ContentSignatures\n"));
        return NS_ERROR_INVALID_SIGNATURE;
      }

      CSVerifier_LOG(("CSVerifier: found a ContentSignature directive\n"));
      mSignature = directive->mValue;
    }
    if (directive->mName.Length() == certChainURL_var.Length() &&
        directive->mName.EqualsIgnoreCase(certChainURL_var.get(),
                                          certChainURL_var.Length())) {
      if (!mCertChainURL.IsEmpty()) {
        CSVerifier_LOG(("CSVerifier: found two x5u values\n"));
        return NS_ERROR_INVALID_SIGNATURE;
      }

      CSVerifier_LOG(("CSVerifier: found an x5u directive\n"));
      mCertChainURL = directive->mValue;
    }
  }

  // we have to ensure that we found a signature at this point
  if (mSignature.IsEmpty()) {
    CSVerifier_LOG(("CSVerifier: got a Content-Signature header but didn't find a signature.\n"));
    return NS_ERROR_FAILURE;
  }

  // Bug 769521: We have to change b64 url to regular encoding as long as we
  // don't have a b64 url decoder. This should change soon, but in the meantime
  // we have to live with this.
  mSignature.ReplaceChar('-', '+');
  mSignature.ReplaceChar('_', '/');

  return NS_OK;
}

/* nsIStreamListener implementation */

NS_IMETHODIMP
ContentSignatureVerifier::OnStartRequest(nsIRequest* aRequest,
                                         nsISupports* aContext)
{
  MOZ_ASSERT(NS_IsMainThread());
  return NS_OK;
}

NS_IMETHODIMP
ContentSignatureVerifier::OnStopRequest(nsIRequest* aRequest,
                                        nsISupports* aContext, nsresult aStatus)
{
  MOZ_ASSERT(NS_IsMainThread());
  nsCOMPtr<nsIContentSignatureReceiverCallback> callback;
  callback.swap(mCallback);
  nsresult rv;

  // Check HTTP status code and return if it's not 200.
  nsCOMPtr<nsIHttpChannel> http = do_QueryInterface(aRequest, &rv);
  uint32_t httpResponseCode;
  if (NS_FAILED(rv) || NS_FAILED(http->GetResponseStatus(&httpResponseCode)) ||
      httpResponseCode != 200) {
    callback->ContextCreated(false);
    return NS_OK;
  }

  if (NS_FAILED(aStatus)) {
    callback->ContextCreated(false);
    return NS_OK;
  }

  nsAutoCString certChain;
  for (uint32_t i = 0; i < mCertChain.Length(); ++i) {
    certChain.Append(mCertChain[i]);
  }

  // We got the cert chain now. Let's create the context.
  rv = CreateContextInternal(NS_LITERAL_CSTRING(""), certChain, mName);
  if (NS_FAILED(rv)) {
    callback->ContextCreated(false);
    return NS_OK;
  }

  mHasCertChain = true;
  callback->ContextCreated(true);
  return NS_OK;
}

NS_IMETHODIMP
ContentSignatureVerifier::OnDataAvailable(nsIRequest* aRequest,
                                          nsISupports* aContext,
                                          nsIInputStream* aInputStream,
                                          uint64_t aOffset, uint32_t aCount)
{
  MOZ_ASSERT(NS_IsMainThread());
  nsAutoCString buffer;

  nsresult rv = NS_ConsumeStream(aInputStream, aCount, buffer);
  if (NS_FAILED(rv)) {
    return rv;
  }

  if (!mCertChain.AppendElement(buffer, fallible)) {
    mCertChain.TruncateLength(0);
    return NS_ERROR_OUT_OF_MEMORY;
  }

  return NS_OK;
}

NS_IMETHODIMP
ContentSignatureVerifier::GetInterface(const nsIID& uuid, void** result)
{
  return QueryInterface(uuid, result);
}