/* 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 "nsIX509CertDB.h"

#include "CryptoTask.h"
#include "QWACTrustDomain.h"
#include "mozilla/dom/Promise.h"
#include "mozpkix/pkix.h"
#include "mozpkix/pkixder.h"
#include "mozpkix/pkixnss.h"
#include "mozpkix/pkixtypes.h"
#include "mozpkix/pkixutil.h"
#include "nsIX509Cert.h"
#include "nsNSSCertificateDB.h"

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

using mozilla::dom::Promise;

class VerifyQWACTask : public mozilla::CryptoTask {
 public:
  VerifyQWACTask(nsIX509CertDB::QWACType aType, nsIX509Cert* aCert,
                 const nsACString& aHostname,
                 const nsTArray<RefPtr<nsIX509Cert>>& aCollectedCerts,
                 RefPtr<Promise>& aPromise)
      : mType(aType),
        mCert(aCert),
        mHostname(aHostname),
        mCollectedCerts(aCollectedCerts.Clone()),
        mPromise(new nsMainThreadPtrHolder<Promise>("VerifyQWACTask::mPromise",
                                                    aPromise)),
        mVerified(false) {}

 private:
  virtual nsresult CalculateResult() override;
  virtual void CallCallback(nsresult rv) override;

  nsIX509CertDB::QWACType mType;
  RefPtr<nsIX509Cert> mCert;
  nsCString mHostname;
  nsTArray<RefPtr<nsIX509Cert>> mCollectedCerts;
  nsMainThreadPtrHandle<Promise> mPromise;

  bool mVerified;
};

// Does this certificate have the correct qcStatements ("qualified certificate
// statements") to be a QWAC ("qualified website authentication certificate")?
// ETSI EN 319 412-5 Clauses 4.2.1 and 4.2.3 state that a certificate issued in
// compliance with Annex IV of Regulation (EU) No 910/2014 (i.e. a QWAC) has
//   1) a QCStatement with statementId equal to id-etsi-qsc-QcCompliance and
//      an omitted statementInfo, and
//   2) a QCStatement with statementId equal to id-etsi-qcs-QcType and a
//      statementInfo of length one that contains the id-etsi-qct-web
//      identifier.
bool CertHasQWACSQCStatements(Input cert) {
  using namespace mozilla::pkix::der;

  // python DottedOIDToCode.py id-etsi-qcs-QcCompliance 0.4.0.1862.1.1
  static const uint8_t id_etsi_qcs_QcCompliance[] = {0x04, 0x00, 0x8e,
                                                     0x46, 0x01, 0x01};

  // python DottedOIDToCode.py id-etsi-qcs-QcType 0.4.0.1862.1.6
  static const uint8_t id_etsi_qcs_QcType[] = {0x04, 0x00, 0x8e,
                                               0x46, 0x01, 0x06};

  // python DottedOIDToCode.py id-etsi-qct-web 0.4.0.1862.1.6.3
  static const uint8_t id_etsi_qct_web[] = {0x04, 0x00, 0x8e, 0x46,
                                            0x01, 0x06, 0x03};

  BackCert backCert(cert, EndEntityOrCA::MustBeEndEntity, nullptr);
  if (backCert.Init() != Success) {
    return false;
  }
  const Input* qcStatementsInput(backCert.GetQCStatements());
  if (!qcStatementsInput) {
    return false;
  }
  Reader qcStatements(*qcStatementsInput);
  // QCStatements ::= SEQUENCE OF QCStatement
  // QCStatement ::= SEQUENCE {
  //     statementId   QC-STATEMENT.&Id({SupportedStatements}),
  //     statementInfo QC-STATEMENT.&Type
  //     ({SupportedStatements}{@statementId}) OPTIONAL }
  //
  // SupportedStatements QC-STATEMENT ::= { qcStatement-1,...}
  bool foundQCComplianceStatement = false;
  bool foundQCTypeStatementWithWebType = false;
  mozilla::pkix::Result rv =
      NestedOf(qcStatements, SEQUENCE, SEQUENCE, EmptyAllowed::No,
               [&](Reader& qcStatementContents) {
                 Reader statementId;
                 mozilla::pkix::Result rv = ExpectTagAndGetValue(
                     qcStatementContents, OIDTag, statementId);
                 if (rv != Success) {
                   return rv;
                 }
                 if (statementId.MatchRest(id_etsi_qcs_QcCompliance)) {
                   foundQCComplianceStatement = true;
                   return End(qcStatementContents);
                 }
                 if (statementId.MatchRest(id_etsi_qcs_QcType)) {
                   Reader supportedStatementsContents;
                   rv = ExpectTagAndGetValue(qcStatementContents, SEQUENCE,
                                             supportedStatementsContents);
                   if (rv != Success) {
                     return rv;
                   }
                   Reader supportedStatementId;
                   rv = ExpectTagAndGetValue(supportedStatementsContents,
                                             OIDTag, supportedStatementId);
                   if (supportedStatementId.MatchRest(id_etsi_qct_web)) {
                     foundQCTypeStatementWithWebType = true;
                   }
                   rv = End(supportedStatementsContents);
                   if (rv != Success) {
                     return rv;
                   }
                   return End(qcStatementContents);
                 }
                 // Ignore the contents of unknown qcStatements.
                 qcStatementContents.SkipToEnd();
                 return Success;
               });
  if (rv != Success) {
    return false;
  }
  if (!qcStatements.AtEnd()) {
    return false;
  }
  return foundQCComplianceStatement && foundQCTypeStatementWithWebType;
}

// Helper function to determine if a certificate has a policy from the given
// list of acceptable policies.
bool CertHasPolicyFrom(Input cert, const nsTArray<Input>& policies) {
  using namespace mozilla::pkix::der;

  BackCert backCert(cert, EndEntityOrCA::MustBeEndEntity, nullptr);
  if (backCert.Init() != Success) {
    return false;
  }
  const Input* certificatePoliciesInput(backCert.GetCertificatePolicies());
  if (!certificatePoliciesInput) {
    return false;
  }
  Reader certificatePolicies(*certificatePoliciesInput);
  // certificatePolicies ::= SEQUENCE SIZE (1..MAX) OF PolicyInformation
  // PolicyInformation ::= SEQUENCE {
  //   policyIdentifier   CertPolicyId,
  //   policyQualifiers   SEQUENCE SIZE (1..MAX) OF
  //                           PolicyQualifierInfo OPTIONAL }
  // }
  // CertPolicyId ::= OBJECT IDENTIFIER
  bool foundPolicy = false;
  mozilla::pkix::Result rv =
      NestedOf(certificatePolicies, SEQUENCE, SEQUENCE, EmptyAllowed::No,
               [&](Reader& policyInformationContents) {
                 Reader policyIdentifier;
                 mozilla::pkix::Result rv = ExpectTagAndGetValue(
                     policyInformationContents, OIDTag, policyIdentifier);
                 if (rv != Success) {
                   return rv;
                 }
                 for (const auto& policy : policies) {
                   if (policyIdentifier.MatchRest(policy)) {
                     foundPolicy = true;
                   }
                 }

                 // The policy may have policyQualifiers. For unrelated
                 // policies, it doesn't make sense to validate the qualifiers.
                 // For related policies, the relevant specifications don't
                 // mention qualifiers one way or another. For maximum
                 // compatibility, we don't validate qualifiers if present. As
                 // a consequence, `policyInformationContents` must be skipped
                 // to the end to consume its input (so `NestedOf` won't return
                 // an error in such cases).
                 policyInformationContents.SkipToEnd();

                 return Success;
               });
  if (rv != Success) {
    return false;
  }
  if (!certificatePolicies.AtEnd()) {
    return false;
  }
  return foundPolicy;
}

// For 1-QWACs, ETSI TS 119 411-5 V2.1.1 clause 6.1.2 ("Validation of QWACs")
// item 5 references clause 4.1.2, which references clause 4.1.1, which states
// that such certificates must have either the QEVCP-w or QNCP-w policy as
// specified in ETSI EN 319 411-2.
bool CertHas1QWACPolicy(Input cert) {
  // QEVCP-w is itu-t(0) identified-organization(4) etsi(0)
  //   qualified-certificate-policies(194112) policy-identifiers(1) qcp-web (4)
  // python DottedOIDToCode.py qevcp-w 0.4.0.194112.1.4
  static const uint8_t qevcp_w[] = {0x04, 0x00, 0x8b, 0xec, 0x40, 0x01, 0x04};

  // QNCP-w is itu-t(0) identified-organization(4) etsi(0)
  //   qualified-certificate-policies(194112) policy-identifiers(1) qncp-web (5)
  // python DottedOIDToCode.py qncp-w 0.4.0.194112.1.5
  static const uint8_t qncp_w[] = {0x04, 0x00, 0x8b, 0xec, 0x40, 0x01, 0x05};

  return CertHasPolicyFrom(cert, {Input(qevcp_w), Input(qncp_w)});
}

// For 2-QWACs, ETSI TS 119 411-5 V2.1.1 clause 6.1.2 ("Validation of QWACs")
// item 5 references clause 4.2.2, which references clause 4.2.1, which states
// that such certificates must have the QNCP-w-gen policy as specified in ETSI
// EN 319 411-2.
bool CertHas2QWACPolicy(Input cert) {
  // QEVCP-w-gen is itu-t(0) identified-organization(4) etsi(0)
  //   qualified-certificate-policies(194112) policy-identifiers(1)
  //   qncp-web-gen (6)
  // python DottedOIDToCode.py qevcp-w-gen 0.4.0.194112.1.6
  static const uint8_t qevcp_w_gen[] = {0x04, 0x00, 0x8b, 0xec,
                                        0x40, 0x01, 0x06};

  return CertHasPolicyFrom(cert, {Input(qevcp_w_gen)});
}

// ETSI TS 119 411-5 V2.1.1 states that "The 2-QWAC certificate shall be issued
// in accordance with ETSI EN 319 412-4 [4] for the relevant certificate policy
// as identified in clause 4.2.1 of the present document, except as described
// below:
//   * the extKeyUsage value shall only assert the extendedKeyUsage purpose of
//     id-kp-tls-binding as specified in Annex A."
// This is interpreted to mean the 2-QWAC certificate must have an
// extendedKeyUsage extension and it must contain only id-kp-tls-binding, and
// that there are no particular restrictions or requirements of the other
// certificates in the chain with regard to EKU extensions.
bool CertOnlyHasTLSBindingEKU(Input cert) {
  using namespace mozilla::pkix::der;

  // ETSI TS 119 411-5 V2.1.1 Annex A:
  // id-tlsBinding OBJECT IDENTIFIER ::= { itu-t(0) identified-organization(4)
  //   etsi(0) id-qwacImplementation(194115) tls-binding (1) }
  // id-kp-tls-binding OBJECT IDENTIFIER ::= { id-tlsBinding
  //   id-kp-tls-binding(0) }
  // python DottedOIDToCode.py id-kp-tls-binding 0.4.0.194115.1.0
  static const uint8_t id_kp_tls_binding[] = {0x04, 0x00, 0x8b, 0xec,
                                              0x43, 0x01, 0x00};

  BackCert backCert(cert, EndEntityOrCA::MustBeEndEntity, nullptr);
  if (backCert.Init() != Success) {
    return false;
  }
  const Input* ekuInput(backCert.GetExtKeyUsage());
  if (!ekuInput) {
    return false;
  }
  Reader eku(*ekuInput);
  // Normally, the extended key usage extension is defined like so:
  //   ExtKeyUsageSyntax ::= SEQUENCE SIZE (1..MAX) OF KeyPurposeId
  //   KeyPurposeId ::= OBJECT IDENTIFIER
  // That is, it consists of a SEQUENCE of OBJECT IDENTIFIERs, where each OID
  // identifies a key purpose. However, for 2-QWACs, the EKU must consist of
  // exactly one key purpose ID of id-kp-tls-binding.
  mozilla::pkix::Result rv = Nested(eku, SEQUENCE, OIDTag, [&](Reader& r) {
    if (r.MatchRest(id_kp_tls_binding)) {
      return Success;
    }
    return mozilla::pkix::Result::ERROR_INADEQUATE_CERT_TYPE;
  });
  if (rv != Success) {
    return false;
  }
  return eku.AtEnd();
}

nsresult VerifyQWACTask::CalculateResult() {
  mozilla::psm::QWACTrustDomain trustDomain(mCollectedCerts);
  nsTArray<uint8_t> certDER;
  nsresult rv = mCert->GetRawDER(certDER);
  if (NS_FAILED(rv)) {
    return rv;
  }
  Input cert;
  if (cert.Init(certDER.Elements(), certDER.Length()) != Success) {
    return NS_ERROR_FAILURE;
  }
  if (!CertHasQWACSQCStatements(cert)) {
    return NS_OK;
  }
  if (mType == nsIX509CertDB::QWACType::OneQWAC) {
    if (!CertHas1QWACPolicy(cert)) {
      return NS_OK;
    }
  } else if (mType == nsIX509CertDB::QWACType::TwoQWAC) {
    if (!CertHas2QWACPolicy(cert)) {
      return NS_OK;
    }
    if (!CertOnlyHasTLSBindingEKU(cert)) {
      return NS_OK;
    }
  } else {
    MOZ_ASSERT_UNREACHABLE("unhandled QWAC type");
    return NS_ERROR_FAILURE;
  }

  if (BuildCertChain(trustDomain, cert, Now(), EndEntityOrCA::MustBeEndEntity,
                     KeyUsage::noParticularKeyUsageRequired,
                     KeyPurposeId::anyExtendedKeyUsage, CertPolicyId::anyPolicy,
                     nullptr) != Success) {
    return NS_OK;
  }

  // For 1-QWACs, the hostname should have already been validated in the TLS
  // handshake. However, this operation is not expensive, and it ensures all
  // required checks have been done, in case 1-QWACs are ever re-used in a
  // different context.
  Input hostname;
  if (hostname.Init(mozilla::BitwiseCast<const uint8_t*, const char*>(
                        mHostname.BeginReading()),
                    mHostname.Length()) != Success) {
    return NS_OK;
  }
  // According to ETSI EN 319 412-4 V1.4.1 section 4, certificates following
  // EVCP or QEVCP-w (which includes 1-QWACs) are subject to the CA/Browser
  // Forum's EV Guidelines, which incorporates the Baseline Requirements.
  // Certificates following QNCP-w-gen (which includes 2-QWACs) are subject to
  // the Baseline Requirements with respect to the subject alternative name
  // extension.
  if (CheckCertHostname(cert, hostname) != Success) {
    return NS_OK;
  }

  mVerified = true;
  return NS_OK;
}

void VerifyQWACTask::CallCallback(nsresult rv) {
  if (NS_FAILED(rv)) {
    mPromise->MaybeReject(rv);
  } else {
    mPromise->MaybeResolve(mVerified);
  }
}

NS_IMETHODIMP
nsNSSCertificateDB::AsyncVerifyQWAC(
    QWACType aType, nsIX509Cert* aCert, const nsACString& aHostname,
    const nsTArray<RefPtr<nsIX509Cert>>& aCollectedCerts, JSContext* aCx,
    mozilla::dom::Promise** aPromise) {
  NS_ENSURE_ARG_POINTER(aCx);

  nsIGlobalObject* globalObject = xpc::CurrentNativeGlobal(aCx);
  if (!globalObject) {
    return NS_ERROR_UNEXPECTED;
  }
  mozilla::ErrorResult result;
  RefPtr<Promise> promise = Promise::Create(globalObject, result);
  if (result.Failed()) {
    return result.StealNSResult();
  }

  RefPtr<VerifyQWACTask> task(
      new VerifyQWACTask(aType, aCert, aHostname, aCollectedCerts, promise));
  nsresult rv = task->Dispatch();
  if (NS_FAILED(rv)) {
    return rv;
  }

  promise.forget(aPromise);
  return NS_OK;
}
