/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=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 "mozilla/dom/RTCCertificate.h"

#include <cmath>
#include "cert.h"
#include "jsapi.h"
#include "mozilla/dom/CryptoKey.h"
#include "mozilla/dom/RTCCertificateBinding.h"
#include "mozilla/dom/WebCryptoCommon.h"
#include "mozilla/dom/WebCryptoTask.h"
#include "mozilla/Sprintf.h"

#include <cstdio>

namespace mozilla {
namespace dom {

#define RTCCERTIFICATE_SC_VERSION 0x00000001

NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(RTCCertificate, mGlobal)
NS_IMPL_CYCLE_COLLECTING_ADDREF(RTCCertificate)
NS_IMPL_CYCLE_COLLECTING_RELEASE(RTCCertificate)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(RTCCertificate)
  NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
  NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END

// Note: explicit casts necessary to avoid
//       warning C4307: '*' : integral constant overflow
#define ONE_DAY PRTime(PR_USEC_PER_SEC) * PRTime(60) /*sec*/ \
  * PRTime(60) /*min*/ * PRTime(24) /*hours*/
#define EXPIRATION_DEFAULT ONE_DAY * PRTime(30)
#define EXPIRATION_SLACK ONE_DAY
#define EXPIRATION_MAX ONE_DAY * PRTime(365) /*year*/

const size_t RTCCertificateCommonNameLength = 16;
const size_t RTCCertificateMinRsaSize = 1024;

class GenerateRTCCertificateTask : public GenerateAsymmetricKeyTask
{
public:
  GenerateRTCCertificateTask(nsIGlobalObject* aGlobal, JSContext* aCx,
                             const ObjectOrString& aAlgorithm,
                             const Sequence<nsString>& aKeyUsages,
                             PRTime aExpires)
      : GenerateAsymmetricKeyTask(aGlobal, aCx, aAlgorithm, true, aKeyUsages),
        mExpires(aExpires),
        mAuthType(ssl_kea_null),
        mCertificate(nullptr),
        mSignatureAlg(SEC_OID_UNKNOWN)
  {
  }

private:
  PRTime mExpires;
  SSLKEAType mAuthType;
  UniqueCERTCertificate mCertificate;
  SECOidTag mSignatureAlg;

  static CERTName* GenerateRandomName(PK11SlotInfo* aSlot)
  {
    uint8_t randomName[RTCCertificateCommonNameLength];
    SECStatus rv = PK11_GenerateRandomOnSlot(aSlot, randomName,
                                             sizeof(randomName));
    if (rv != SECSuccess) {
      return nullptr;
    }

    char buf[sizeof(randomName) * 2 + 4];
    PL_strncpy(buf, "CN=", 3);
    for (size_t i = 0; i < sizeof(randomName); ++i) {
      snprintf(&buf[i * 2 + 3], 2, "%.2x", randomName[i]);
    }
    buf[sizeof(buf) - 1] = '\0';

    return CERT_AsciiToName(buf);
  }

  nsresult GenerateCertificate()
  {
    ScopedPK11SlotInfo slot(PK11_GetInternalSlot());
    MOZ_ASSERT(slot.get());

    ScopedCERTName subjectName(GenerateRandomName(slot.get()));
    if (!subjectName) {
      return NS_ERROR_DOM_UNKNOWN_ERR;
    }

    ScopedSECKEYPublicKey publicKey(mKeyPair->mPublicKey.get()->GetPublicKey());
    ScopedCERTSubjectPublicKeyInfo spki(
        SECKEY_CreateSubjectPublicKeyInfo(publicKey));
    if (!spki) {
      return NS_ERROR_DOM_UNKNOWN_ERR;
    }

    ScopedCERTCertificateRequest certreq(
        CERT_CreateCertificateRequest(subjectName, spki, nullptr));
    if (!certreq) {
      return NS_ERROR_DOM_UNKNOWN_ERR;
    }

    PRTime now = PR_Now();
    PRTime notBefore = now - EXPIRATION_SLACK;
    mExpires += now;

    ScopedCERTValidity validity(CERT_CreateValidity(notBefore, mExpires));
    if (!validity) {
      return NS_ERROR_DOM_UNKNOWN_ERR;
    }

    unsigned long serial;
    // Note: This serial in principle could collide, but it's unlikely, and we
    // don't expect anyone to be validating certificates anyway.
    SECStatus rv =
        PK11_GenerateRandomOnSlot(slot,
                                  reinterpret_cast<unsigned char *>(&serial),
                                  sizeof(serial));
    if (rv != SECSuccess) {
      return NS_ERROR_DOM_UNKNOWN_ERR;
    }

    CERTCertificate* cert = CERT_CreateCertificate(serial, subjectName,
                                                   validity, certreq);
    if (!cert) {
      return NS_ERROR_DOM_UNKNOWN_ERR;
    }
    mCertificate.reset(cert);
    return NS_OK;
  }

  nsresult SignCertificate()
  {
    MOZ_ASSERT(mSignatureAlg != SEC_OID_UNKNOWN);
    PLArenaPool *arena = mCertificate->arena;

    SECStatus rv = SECOID_SetAlgorithmID(arena, &mCertificate->signature,
                                         mSignatureAlg, nullptr);
    if (rv != SECSuccess) {
      return NS_ERROR_DOM_UNKNOWN_ERR;
    }

    // Set version to X509v3.
    *(mCertificate->version.data) = SEC_CERTIFICATE_VERSION_3;
    mCertificate->version.len = 1;

    SECItem innerDER = { siBuffer, nullptr, 0 };
    if (!SEC_ASN1EncodeItem(arena, &innerDER, mCertificate.get(),
                            SEC_ASN1_GET(CERT_CertificateTemplate))) {
      return NS_ERROR_DOM_UNKNOWN_ERR;
    }

    SECItem *signedCert = PORT_ArenaZNew(arena, SECItem);
    if (!signedCert) {
      return NS_ERROR_DOM_UNKNOWN_ERR;
    }

    ScopedSECKEYPrivateKey privateKey(mKeyPair->mPrivateKey.get()->GetPrivateKey());
    rv = SEC_DerSignData(arena, signedCert, innerDER.data, innerDER.len,
                         privateKey, mSignatureAlg);
    if (rv != SECSuccess) {
      return NS_ERROR_DOM_UNKNOWN_ERR;
    }
    mCertificate->derCert = *signedCert;
    return NS_OK;
  }

  nsresult BeforeCrypto() override
  {
    if (mAlgName.EqualsLiteral(WEBCRYPTO_ALG_RSASSA_PKCS1)) {
      // Double check that size is OK.
      auto sz = static_cast<size_t>(mRsaParams.keySizeInBits);
      if (sz < RTCCertificateMinRsaSize) {
        return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
      }

      KeyAlgorithmProxy& alg = mKeyPair->mPublicKey.get()->Algorithm();
      if (alg.mType != KeyAlgorithmProxy::RSA ||
          !alg.mRsa.mHash.mName.EqualsLiteral(WEBCRYPTO_ALG_SHA256)) {
        return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
      }

      mSignatureAlg = SEC_OID_PKCS1_SHA256_WITH_RSA_ENCRYPTION;
      mAuthType = ssl_kea_rsa;

    } else if (mAlgName.EqualsLiteral(WEBCRYPTO_ALG_ECDSA)) {
      // We only support good curves in WebCrypto.
      // If that ever changes, check that a good one was chosen.

      mSignatureAlg = SEC_OID_ANSIX962_ECDSA_SHA256_SIGNATURE;
      mAuthType = ssl_kea_ecdh;
    } else {
      return NS_ERROR_DOM_NOT_SUPPORTED_ERR;
    }
    return NS_OK;
  }

  nsresult DoCrypto() override
  {
    nsresult rv = GenerateAsymmetricKeyTask::DoCrypto();
    NS_ENSURE_SUCCESS(rv, rv);

    rv = GenerateCertificate();
    NS_ENSURE_SUCCESS(rv, rv);

    rv = SignCertificate();
    NS_ENSURE_SUCCESS(rv, rv);

    return NS_OK;
  }

  virtual void Resolve() override
  {
    // Make copies of the private key and certificate, otherwise, when this
    // object is deleted, the structures they reference will be deleted too.
    SECKEYPrivateKey* key = mKeyPair->mPrivateKey.get()->GetPrivateKey();
    CERTCertificate* cert = CERT_DupCertificate(mCertificate.get());
    RefPtr<RTCCertificate> result =
        new RTCCertificate(mResultPromise->GetParentObject(),
                           key, cert, mAuthType, mExpires);
    mResultPromise->MaybeResolve(result);
  }
};

static PRTime
ReadExpires(JSContext* aCx, const ObjectOrString& aOptions,
            ErrorResult& aRv)
{
  // This conversion might fail, but we don't really care; use the default.
  // If this isn't an object, or it doesn't coerce into the right type,
  // then we won't get the |expires| value.  Either will be caught later.
  RTCCertificateExpiration expiration;
  if (!aOptions.IsObject()) {
    return EXPIRATION_DEFAULT;
  }
  JS::RootedValue value(aCx, JS::ObjectValue(*aOptions.GetAsObject()));
  if (!expiration.Init(aCx, value)) {
    aRv.NoteJSContextException(aCx);
    return 0;
  }

  if (!expiration.mExpires.WasPassed()) {
    return EXPIRATION_DEFAULT;
  }
  static const uint64_t max =
      static_cast<uint64_t>(EXPIRATION_MAX / PR_USEC_PER_MSEC);
  if (expiration.mExpires.Value() > max) {
    return EXPIRATION_MAX;
  }
  return static_cast<PRTime>(expiration.mExpires.Value() * PR_USEC_PER_MSEC);
}

already_AddRefed<Promise>
RTCCertificate::GenerateCertificate(
    const GlobalObject& aGlobal, const ObjectOrString& aOptions,
    ErrorResult& aRv, JSCompartment* aCompartment)
{
  nsIGlobalObject* global = xpc::NativeGlobal(aGlobal.Get());
  RefPtr<Promise> p = Promise::Create(global, aRv);
  if (aRv.Failed()) {
    return nullptr;
  }
  Sequence<nsString> usages;
  if (!usages.AppendElement(NS_LITERAL_STRING("sign"), fallible)) {
    aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
    return nullptr;
  }

  PRTime expires = ReadExpires(aGlobal.Context(), aOptions, aRv);
  if (aRv.Failed()) {
    return nullptr;
  }
  RefPtr<WebCryptoTask> task =
      new GenerateRTCCertificateTask(global, aGlobal.Context(),
                                     aOptions, usages, expires);
  task->DispatchWithPromise(p);
  return p.forget();
}

RTCCertificate::RTCCertificate(nsIGlobalObject* aGlobal)
    : mGlobal(aGlobal),
      mPrivateKey(nullptr),
      mCertificate(nullptr),
      mAuthType(ssl_kea_null),
      mExpires(0)
{
}

RTCCertificate::RTCCertificate(nsIGlobalObject* aGlobal,
                               SECKEYPrivateKey* aPrivateKey,
                               CERTCertificate* aCertificate,
                               SSLKEAType aAuthType,
                               PRTime aExpires)
    : mGlobal(aGlobal),
      mPrivateKey(aPrivateKey),
      mCertificate(aCertificate),
      mAuthType(aAuthType),
      mExpires(aExpires)
{
}

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

// This creates some interesting lifecycle consequences, since the DtlsIdentity
// holds NSS objects, but does not implement nsNSSShutDownObject.

// Unfortunately, the code that uses DtlsIdentity cannot always use that lock
// due to external linkage requirements.  Therefore, the lock is held on this
// object instead.  Consequently, the DtlsIdentity that this method returns must
// have a lifetime that is strictly shorter than the RTCCertificate.
//
// RTCPeerConnection provides this guarantee by holding a strong reference to
// the RTCCertificate.  It will cleanup any DtlsIdentity instances that it
// creates before the RTCCertificate reference is released.
RefPtr<DtlsIdentity>
RTCCertificate::CreateDtlsIdentity() const
{
  nsNSSShutDownPreventionLock locker;
  if (isAlreadyShutDown() || !mPrivateKey || !mCertificate) {
    return nullptr;
  }
  SECKEYPrivateKey* key = SECKEY_CopyPrivateKey(mPrivateKey.get());
  CERTCertificate* cert = CERT_DupCertificate(mCertificate.get());
  RefPtr<DtlsIdentity> id = new DtlsIdentity(key, cert, mAuthType);
  return id;
}

JSObject*
RTCCertificate::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto)
{
  return RTCCertificateBinding::Wrap(aCx, this, aGivenProto);
}

void
RTCCertificate::virtualDestroyNSSReference()
{
  destructorSafeDestroyNSSReference();
}

void
RTCCertificate::destructorSafeDestroyNSSReference()
{
  mPrivateKey.reset();
  mCertificate.reset();
}

bool
RTCCertificate::WritePrivateKey(JSStructuredCloneWriter* aWriter,
                                const nsNSSShutDownPreventionLock& aLockProof) const
{
  JsonWebKey jwk;
  nsresult rv = CryptoKey::PrivateKeyToJwk(mPrivateKey.get(), jwk, aLockProof);
  if (NS_FAILED(rv)) {
    return false;
  }
  nsString json;
  if (!jwk.ToJSON(json)) {
    return false;
  }
  return WriteString(aWriter, json);
}

bool
RTCCertificate::WriteCertificate(JSStructuredCloneWriter* aWriter,
                                 const nsNSSShutDownPreventionLock& /*proof*/) const
{
  ScopedCERTCertificateList certs(CERT_CertListFromCert(mCertificate.get()));
  if (!certs || certs->len <= 0) {
    return false;
  }
  if (!JS_WriteUint32Pair(aWriter, certs->certs[0].len, 0)) {
    return false;
  }
  return JS_WriteBytes(aWriter, certs->certs[0].data, certs->certs[0].len);
}

bool
RTCCertificate::WriteStructuredClone(JSStructuredCloneWriter* aWriter) const
{
  nsNSSShutDownPreventionLock locker;
  if (isAlreadyShutDown() || !mPrivateKey || !mCertificate) {
    return false;
  }

  return JS_WriteUint32Pair(aWriter, RTCCERTIFICATE_SC_VERSION, mAuthType) &&
      JS_WriteUint32Pair(aWriter, (mExpires >> 32) & 0xffffffff,
                         mExpires & 0xffffffff) &&
      WritePrivateKey(aWriter, locker) &&
      WriteCertificate(aWriter, locker);
}

bool
RTCCertificate::ReadPrivateKey(JSStructuredCloneReader* aReader,
                               const nsNSSShutDownPreventionLock& aLockProof)
{
  nsString json;
  if (!ReadString(aReader, json)) {
    return false;
  }
  JsonWebKey jwk;
  if (!jwk.Init(json)) {
    return false;
  }
  mPrivateKey.reset(CryptoKey::PrivateKeyFromJwk(jwk, aLockProof));
  return !!mPrivateKey;
}

bool
RTCCertificate::ReadCertificate(JSStructuredCloneReader* aReader,
                                const nsNSSShutDownPreventionLock& /*proof*/)
{
  CryptoBuffer cert;
  if (!ReadBuffer(aReader, cert) || cert.Length() == 0) {
    return false;
  }

  SECItem der = { siBuffer, cert.Elements(),
                  static_cast<unsigned int>(cert.Length()) };
  mCertificate.reset(CERT_NewTempCertificate(CERT_GetDefaultCertDB(),
                                             &der, nullptr, true, true));
  return !!mCertificate;
}

bool
RTCCertificate::ReadStructuredClone(JSStructuredCloneReader* aReader)
{
  nsNSSShutDownPreventionLock locker;
  if (isAlreadyShutDown()) {
    return false;
  }

  uint32_t version, authType;
  if (!JS_ReadUint32Pair(aReader, &version, &authType) ||
      version != RTCCERTIFICATE_SC_VERSION) {
    return false;
  }
  mAuthType = static_cast<SSLKEAType>(authType);

  uint32_t high, low;
  if (!JS_ReadUint32Pair(aReader, &high, &low)) {
    return false;
  }
  mExpires = static_cast<PRTime>(high) << 32 | low;

  return ReadPrivateKey(aReader, locker) &&
      ReadCertificate(aReader, locker);
}

} // namespace dom
} // namespace mozilla