/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
 *
 * 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 "EnterpriseRoots.h"

#include "mozilla/ArrayUtils.h"
#include "mozilla/Logging.h"
#include "mozilla/Unused.h"
#include "mozpkix/Result.h"
#include "nsThreadUtils.h"

#ifdef MOZ_WIDGET_ANDROID
#  include "mozilla/java/EnterpriseRootsWrappers.h"
#endif  // MOZ_WIDGET_ANDROID

#ifdef XP_MACOSX
#  include <Security/Security.h>
#  include "KeychainSecret.h"  // for ScopedCFType

#  include "nsCocoaFeatures.h"
#endif  // XP_MACOSX

#ifdef XP_WIN
#  include <windows.h>
#  include <wincrypt.h>
#endif  // XP_WIN

extern mozilla::LazyLogModule gPIPNSSLog;

using namespace mozilla;

nsresult EnterpriseCert::Init(const uint8_t* data, size_t len, bool isRoot) {
  mDER.clear();
  if (!mDER.append(data, len)) {
    return NS_ERROR_OUT_OF_MEMORY;
  }
  mIsRoot = isRoot;

  return NS_OK;
}

nsresult EnterpriseCert::Init(const EnterpriseCert& orig) {
  return Init(orig.mDER.begin(), orig.mDER.length(), orig.mIsRoot);
}

nsresult EnterpriseCert::CopyBytes(nsTArray<uint8_t>& dest) const {
  dest.Clear();
  // XXX(Bug 1631371) Check if this should use a fallible operation as it
  // pretended earlier, or change the return type to void.
  dest.AppendElements(mDER.begin(), mDER.length());
  return NS_OK;
}

pkix::Result EnterpriseCert::GetInput(pkix::Input& input) const {
  return input.Init(mDER.begin(), mDER.length());
}

bool EnterpriseCert::GetIsRoot() const { return mIsRoot; }

#ifdef XP_WIN
const wchar_t* kWindowsDefaultRootStoreNames[] = {L"ROOT", L"CA"};

// Helper function to determine if the OS considers the given certificate to be
// a trust anchor for TLS server auth certificates. This is to be used in the
// context of importing what are presumed to be root certificates from the OS.
// If this function returns true but it turns out that the given certificate is
// in some way unsuitable to issue certificates, mozilla::pkix will never build
// a valid chain that includes the certificate, so importing it even if it
// isn't a valid CA poses no risk.
static void CertIsTrustAnchorForTLSServerAuth(PCCERT_CONTEXT certificate,
                                              bool& isTrusted, bool& isRoot) {
  isTrusted = false;
  isRoot = false;
  MOZ_ASSERT(certificate);
  if (!certificate) {
    return;
  }

  PCCERT_CHAIN_CONTEXT pChainContext = nullptr;
  CERT_ENHKEY_USAGE enhkeyUsage;
  memset(&enhkeyUsage, 0, sizeof(CERT_ENHKEY_USAGE));
  LPCSTR identifiers[] = {
      "1.3.6.1.5.5.7.3.1",  // id-kp-serverAuth
  };
  enhkeyUsage.cUsageIdentifier = ArrayLength(identifiers);
  enhkeyUsage.rgpszUsageIdentifier =
      const_cast<LPSTR*>(identifiers);  // -Wwritable-strings
  CERT_USAGE_MATCH certUsage;
  memset(&certUsage, 0, sizeof(CERT_USAGE_MATCH));
  certUsage.dwType = USAGE_MATCH_TYPE_AND;
  certUsage.Usage = enhkeyUsage;
  CERT_CHAIN_PARA chainPara;
  memset(&chainPara, 0, sizeof(CERT_CHAIN_PARA));
  chainPara.cbSize = sizeof(CERT_CHAIN_PARA);
  chainPara.RequestedUsage = certUsage;
  // Disable anything that could result in network I/O.
  DWORD flags = CERT_CHAIN_REVOCATION_CHECK_CACHE_ONLY |
                CERT_CHAIN_CACHE_ONLY_URL_RETRIEVAL |
                CERT_CHAIN_DISABLE_AUTH_ROOT_AUTO_UPDATE |
                CERT_CHAIN_DISABLE_AIA;
  if (!CertGetCertificateChain(nullptr, certificate, nullptr, nullptr,
                               &chainPara, flags, nullptr, &pChainContext)) {
    MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("CertGetCertificateChain failed"));
    return;
  }
  isTrusted = pChainContext->TrustStatus.dwErrorStatus == CERT_TRUST_NO_ERROR;
  if (isTrusted && pChainContext->cChain > 0) {
    // The so-called "final chain" is what we're after:
    // https://docs.microsoft.com/en-us/windows/desktop/api/wincrypt/ns-wincrypt-_cert_chain_context
    CERT_SIMPLE_CHAIN* finalChain =
        pChainContext->rgpChain[pChainContext->cChain - 1];
    // This is a root if the final chain consists of only one certificate (i.e.
    // this one).
    isRoot = finalChain->cElement == 1;
  }
  CertFreeCertificateChain(pChainContext);
}

// Because HCERTSTORE is just a typedef void*, we can't use any of the nice
// scoped or unique pointer templates. To elaborate, any attempt would
// instantiate those templates with T = void. When T gets used in the context
// of T&, this results in void&, which isn't legal.
class ScopedCertStore final {
 public:
  explicit ScopedCertStore(HCERTSTORE certstore) : certstore(certstore) {}

  ~ScopedCertStore() { CertCloseStore(certstore, 0); }

  HCERTSTORE get() { return certstore; }

 private:
  ScopedCertStore(const ScopedCertStore&) = delete;
  ScopedCertStore& operator=(const ScopedCertStore&) = delete;
  HCERTSTORE certstore;
};

// Loads the enterprise roots at the registry location corresponding to the
// given location flag.
// Supported flags are:
//   CERT_SYSTEM_STORE_LOCAL_MACHINE
//     (for HKLM\SOFTWARE\Microsoft\SystemCertificates)
//   CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY
//     (for HKLM\SOFTWARE\Policy\Microsoft\SystemCertificates)
//   CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE
//     (for HKLM\SOFTWARE\Microsoft\EnterpriseCertificates)
//   CERT_SYSTEM_STORE_CURRENT_USER
//     (for HKCU\SOFTWARE\Microsoft\SystemCertificates)
//   CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY
//     (for HKCU\SOFTWARE\Policy\Microsoft\SystemCertificates)
static void GatherEnterpriseCertsForLocation(DWORD locationFlag,
                                             Vector<EnterpriseCert>& certs) {
  MOZ_ASSERT(locationFlag == CERT_SYSTEM_STORE_LOCAL_MACHINE ||
                 locationFlag == CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY ||
                 locationFlag == CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE ||
                 locationFlag == CERT_SYSTEM_STORE_CURRENT_USER ||
                 locationFlag == CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY,
             "unexpected locationFlag for GatherEnterpriseRootsForLocation");
  if (!(locationFlag == CERT_SYSTEM_STORE_LOCAL_MACHINE ||
        locationFlag == CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY ||
        locationFlag == CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE ||
        locationFlag == CERT_SYSTEM_STORE_CURRENT_USER ||
        locationFlag == CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY)) {
    return;
  }

  DWORD flags =
      locationFlag | CERT_STORE_OPEN_EXISTING_FLAG | CERT_STORE_READONLY_FLAG;
  // The certificate store being opened should consist only of certificates
  // added by a user or administrator and not any certificates that are part
  // of Microsoft's root store program.
  // The 3rd parameter to CertOpenStore should be NULL according to
  // https://msdn.microsoft.com/en-us/library/windows/desktop/aa376559%28v=vs.85%29.aspx
  for (auto name : kWindowsDefaultRootStoreNames) {
    ScopedCertStore enterpriseRootStore(
        CertOpenStore(CERT_STORE_PROV_SYSTEM_REGISTRY_W, 0, NULL, flags, name));
    if (!enterpriseRootStore.get()) {
      MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
              ("failed to open enterprise root store"));
      continue;
    }
    PCCERT_CONTEXT certificate = nullptr;
    uint32_t numImported = 0;
    while ((certificate = CertFindCertificateInStore(
                enterpriseRootStore.get(), X509_ASN_ENCODING, 0, CERT_FIND_ANY,
                nullptr, certificate))) {
      bool isTrusted;
      bool isRoot;
      CertIsTrustAnchorForTLSServerAuth(certificate, isTrusted, isRoot);
      if (!isTrusted) {
        MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
                ("skipping cert not trusted for TLS server auth"));
        continue;
      }
      EnterpriseCert enterpriseCert;
      if (NS_FAILED(enterpriseCert.Init(certificate->pbCertEncoded,
                                        certificate->cbCertEncoded, isRoot))) {
        // Best-effort. We probably ran out of memory.
        continue;
      }
      if (!certs.append(std::move(enterpriseCert))) {
        // Best-effort again.
        continue;
      }
      numImported++;
    }
    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
            ("imported %u certs from %S", numImported, name));
  }
}

static void GatherEnterpriseCertsWindows(Vector<EnterpriseCert>& certs) {
  GatherEnterpriseCertsForLocation(CERT_SYSTEM_STORE_LOCAL_MACHINE, certs);
  GatherEnterpriseCertsForLocation(CERT_SYSTEM_STORE_LOCAL_MACHINE_GROUP_POLICY,
                                   certs);
  GatherEnterpriseCertsForLocation(CERT_SYSTEM_STORE_LOCAL_MACHINE_ENTERPRISE,
                                   certs);
  GatherEnterpriseCertsForLocation(CERT_SYSTEM_STORE_CURRENT_USER, certs);
  GatherEnterpriseCertsForLocation(CERT_SYSTEM_STORE_CURRENT_USER_GROUP_POLICY,
                                   certs);
}
#endif  // XP_WIN

#ifdef XP_MACOSX
OSStatus GatherEnterpriseCertsMacOS(Vector<EnterpriseCert>& certs) {
  // The following builds a search dictionary corresponding to:
  // { class: "certificate",
  //   match limit: "match all",
  //   policy: "SSL (TLS)",
  //   only include trusted certificates: true }
  // This operates on items that have been added to the keychain and thus gives
  // us all 3rd party certificates that have been trusted for SSL (TLS), which
  // is what we want (thus we don't import built-in root certificates that ship
  // with the OS).
  const CFStringRef keys[] = {kSecClass, kSecMatchLimit, kSecMatchPolicy,
                              kSecMatchTrustedOnly};
  // https://developer.apple.com/documentation/security/1392592-secpolicycreatessl
  ScopedCFType<SecPolicyRef> sslPolicy(SecPolicyCreateSSL(true, nullptr));
  const void* values[] = {kSecClassCertificate, kSecMatchLimitAll,
                          sslPolicy.get(), kCFBooleanTrue};
  static_assert(ArrayLength(keys) == ArrayLength(values),
                "mismatched SecItemCopyMatching key/value array sizes");
  // https://developer.apple.com/documentation/corefoundation/1516782-cfdictionarycreate
  ScopedCFType<CFDictionaryRef> searchDictionary(CFDictionaryCreate(
      nullptr, (const void**)&keys, (const void**)&values, ArrayLength(keys),
      &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks));
  CFTypeRef items;
  // https://developer.apple.com/documentation/security/1398306-secitemcopymatching
  OSStatus rv = SecItemCopyMatching(searchDictionary.get(), &items);
  if (rv != errSecSuccess) {
    MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("SecItemCopyMatching failed"));
    return rv;
  }
  // If given a match limit greater than 1 (which we did), SecItemCopyMatching
  // returns a CFArrayRef.
  ScopedCFType<CFArrayRef> arr(reinterpret_cast<CFArrayRef>(items));
  CFIndex count = CFArrayGetCount(arr.get());
  uint32_t numImported = 0;
  for (CFIndex i = 0; i < count; i++) {
    const CFTypeRef c = CFArrayGetValueAtIndex(arr.get(), i);
    SecTrustRef trust;
    rv = SecTrustCreateWithCertificates(c, sslPolicy.get(), &trust);
    if (rv != errSecSuccess) {
      MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
              ("SecTrustCreateWithCertificates failed"));
      continue;
    }
    ScopedCFType<SecTrustRef> trustHandle(trust);
    // Disable AIA chasing to avoid network I/O.
    rv = SecTrustSetNetworkFetchAllowed(trustHandle.get(), false);
    if (rv != errSecSuccess) {
      MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
              ("SecTrustSetNetworkFetchAllowed failed"));
      continue;
    }
    bool isTrusted = false;
    bool fallBackToDeprecatedAPI = true;
    if (nsCocoaFeatures::OnMojaveOrLater()) {
      // This is an awkward way to express what we want, but the compiler
      // complains if we try to put __builtin_available in a compound logical
      // statement.
      if (__builtin_available(macOS 10.14, *)) {
        isTrusted = SecTrustEvaluateWithError(trustHandle.get(), nullptr);
        fallBackToDeprecatedAPI = false;
      }
    }
    if (fallBackToDeprecatedAPI) {
      SecTrustResultType result;
      rv = SecTrustEvaluate(trustHandle.get(), &result);
      if (rv != errSecSuccess) {
        MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("SecTrustEvaluate failed"));
        continue;
      }
      // 'kSecTrustResultProceed' means the trust evaluation succeeded and that
      // this is a trusted certificate.
      // 'kSecTrustResultUnspecified' means the trust evaluation succeeded and
      // that this certificate inherits its trust.
      isTrusted = result == kSecTrustResultProceed ||
                  result == kSecTrustResultUnspecified;
    }
    if (!isTrusted) {
      MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("skipping cert not trusted"));
      continue;
    }
    CFIndex count = SecTrustGetCertificateCount(trustHandle.get());
    bool isRoot = count == 1;

    // Because we asked for certificates, each CFTypeRef in the array is really
    // a SecCertificateRef.
    const SecCertificateRef s = (const SecCertificateRef)c;
    ScopedCFType<CFDataRef> der(SecCertificateCopyData(s));
    EnterpriseCert enterpriseCert;
    if (NS_FAILED(enterpriseCert.Init(CFDataGetBytePtr(der.get()),
                                      CFDataGetLength(der.get()), isRoot))) {
      // Best-effort. We probably ran out of memory.
      continue;
    }
    if (!certs.append(std::move(enterpriseCert))) {
      // Best-effort again.
      continue;
    }
    numImported++;
  }
  MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("imported %u certs", numImported));
  return errSecSuccess;
}
#endif  // XP_MACOSX

#ifdef MOZ_WIDGET_ANDROID
void GatherEnterpriseCertsAndroid(Vector<EnterpriseCert>& certs) {
  if (!jni::IsAvailable()) {
    MOZ_LOG(gPIPNSSLog, LogLevel::Debug, ("JNI not available"));
    return;
  }
  jni::ObjectArray::LocalRef roots =
      java::EnterpriseRoots::GatherEnterpriseRoots();
  for (size_t i = 0; i < roots->Length(); i++) {
    jni::ByteArray::LocalRef root = roots->GetElement(i);
    EnterpriseCert cert;
    // Currently we treat all certificates gleaned from the Android
    // CA store as roots.
    if (NS_SUCCEEDED(cert.Init(
            reinterpret_cast<uint8_t*>(root->GetElements().Elements()),
            root->Length(), true))) {
      Unused << certs.append(std::move(cert));
    }
  }
}
#endif  // MOZ_WIDGET_ANDROID

nsresult GatherEnterpriseCerts(Vector<EnterpriseCert>& certs) {
  MOZ_ASSERT(!NS_IsMainThread());
  if (NS_IsMainThread()) {
    return NS_ERROR_NOT_SAME_THREAD;
  }

  certs.clear();
#ifdef XP_WIN
  GatherEnterpriseCertsWindows(certs);
#endif  // XP_WIN
#ifdef XP_MACOSX
  OSStatus rv = GatherEnterpriseCertsMacOS(certs);
  if (rv != errSecSuccess) {
    return NS_ERROR_FAILURE;
  }
#endif  // XP_MACOSX
#ifdef MOZ_WIDGET_ANDROID
  GatherEnterpriseCertsAndroid(certs);
#endif  // MOZ_WIDGET_ANDROID
  return NS_OK;
}
