/* -*- 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 "CertVerifier.h"
#include "OCSPCache.h"
#include "nss.h"
#include "prerr.h"
#include "prprf.h"
#include "secerr.h"

#include "gtest/gtest.h"

const int MaxCacheEntries = 1024;

class OCSPCacheTest : public ::testing::Test
{
  protected:
    static void SetUpTestCase()
    {
      NSS_NoDB_Init(nullptr);
      mozilla::psm::InitCertVerifierLog();
    }

    mozilla::psm::OCSPCache cache;
};

// Makes a fake certificate with just the fields we need for testing here.
// (And those values are almost entirely bogus.)
// stackCert should be stack-allocated memory.
static void
MakeFakeCert(CERTCertificate* stackCert, const char* subjectValue,
             const char* issuerValue, const char* serialNumberValue,
             const char* publicKeyValue)
{
  stackCert->derSubject.data = (unsigned char*)subjectValue;
  stackCert->derSubject.len = strlen(subjectValue);
  stackCert->derIssuer.data = (unsigned char*)issuerValue;
  stackCert->derIssuer.len = strlen(issuerValue);
  stackCert->serialNumber.data = (unsigned char*)serialNumberValue;
  stackCert->serialNumber.len = strlen(serialNumberValue);
  stackCert->derPublicKey.data = (unsigned char*)publicKeyValue;
  stackCert->derPublicKey.len = strlen(publicKeyValue);
  CERTName *subject = CERT_AsciiToName(subjectValue); // TODO: this will leak...
  ASSERT_TRUE(subject);
  stackCert->subject.arena = subject->arena;
  stackCert->subject.rdns = subject->rdns;
}

static void
PutAndGet(mozilla::psm::OCSPCache& cache, CERTCertificate* subject,
          CERTCertificate* issuer,
          PRErrorCode error, PRTime time)
{
  // The first time is thisUpdate. The second is validUntil.
  // The caller is expecting the validUntil returned with Get
  // to be equal to the passed-in time. Since these values will
  // be different in practice, make thisUpdate less than validUntil.
  ASSERT_TRUE(time >= 10);
  SECStatus rv = cache.Put(subject, issuer, error, time - 10, time);
  ASSERT_TRUE(rv == SECSuccess);
  PRErrorCode errorOut;
  PRTime timeOut;
  ASSERT_TRUE(cache.Get(subject, issuer, errorOut, timeOut));
  ASSERT_TRUE(error == errorOut && time == timeOut);
}

TEST_F(OCSPCacheTest, TestPutAndGet)
{
  SCOPED_TRACE("");
  CERTCertificate subject;
  MakeFakeCert(&subject, "CN=subject1", "CN=issuer1", "001", "key001");
  CERTCertificate issuer;
  MakeFakeCert(&issuer, "CN=issuer1", "CN=issuer1", "000", "key000");
  PutAndGet(cache, &subject, &issuer, 0, PR_Now());
  PRErrorCode errorOut;
  PRTime timeOut;
  ASSERT_FALSE(cache.Get(&issuer, &issuer, errorOut, timeOut));
}

TEST_F(OCSPCacheTest, TestVariousGets)
{
  SCOPED_TRACE("");
  CERTCertificate issuer;
  MakeFakeCert(&issuer, "CN=issuer1", "CN=issuer1", "000", "key000");
  PRTime timeIn = PR_Now();
  for (int i = 0; i < MaxCacheEntries; i++) {
    CERTCertificate subject;
    char subjectBuf[64];
    PR_snprintf(subjectBuf, sizeof(subjectBuf), "CN=subject%04d", i);
    char serialBuf[8];
    PR_snprintf(serialBuf, sizeof(serialBuf), "%04d", i);
    MakeFakeCert(&subject, subjectBuf, "CN=issuer1", serialBuf, "key000");
    PutAndGet(cache, &subject, &issuer, 0, timeIn + i);
  }
  CERTCertificate subject;
  // This will be at the end of the list in the cache
  PRErrorCode errorOut;
  PRTime timeOut;
  MakeFakeCert(&subject, "CN=subject0000", "CN=issuer1", "0000", "key000");
  ASSERT_TRUE(cache.Get(&subject, &issuer, errorOut, timeOut));
  ASSERT_TRUE(errorOut == 0 && timeOut == timeIn);
  // Once we access it, it goes to the front
  ASSERT_TRUE(cache.Get(&subject, &issuer, errorOut, timeOut));
  ASSERT_TRUE(errorOut == 0 && timeOut == timeIn);
  MakeFakeCert(&subject, "CN=subject0512", "CN=issuer1", "0512", "key000");
  // This will be in the middle
  ASSERT_TRUE(cache.Get(&subject, &issuer, errorOut, timeOut));
  ASSERT_TRUE(errorOut == 0 && timeOut == timeIn + 512);
  ASSERT_TRUE(cache.Get(&subject, &issuer, errorOut, timeOut));
  ASSERT_TRUE(errorOut == 0 && timeOut == timeIn + 512);
  // We've never seen this certificate
  MakeFakeCert(&subject, "CN=subject1111", "CN=issuer1", "1111", "key000");
  ASSERT_FALSE(cache.Get(&subject, &issuer, errorOut, timeOut));
}

TEST_F(OCSPCacheTest, TestEviction)
{
  SCOPED_TRACE("");
  CERTCertificate issuer;
  PRTime timeIn = PR_Now();
  MakeFakeCert(&issuer, "CN=issuer1", "CN=issuer1", "000", "key000");
  // By putting more distinct entries in the cache than it can hold,
  // we cause the least recently used entry to be evicted.
  for (int i = 0; i < MaxCacheEntries + 1; i++) {
    CERTCertificate subject;
    char subjectBuf[64];
    PR_snprintf(subjectBuf, sizeof(subjectBuf), "CN=subject%04d", i);
    char serialBuf[8];
    PR_snprintf(serialBuf, sizeof(serialBuf), "%04d", i);
    MakeFakeCert(&subject, subjectBuf, "CN=issuer1", serialBuf, "key000");
    PutAndGet(cache, &subject, &issuer, 0, timeIn + i);
  }
  CERTCertificate evictedSubject;
  MakeFakeCert(&evictedSubject, "CN=subject0000", "CN=issuer1", "0000", "key000");
  PRErrorCode errorOut;
  PRTime timeOut;
  ASSERT_FALSE(cache.Get(&evictedSubject, &issuer, errorOut, timeOut));
}

TEST_F(OCSPCacheTest, TestNoEvictionForRevokedResponses)
{
  SCOPED_TRACE("");
  CERTCertificate issuer;
  MakeFakeCert(&issuer, "CN=issuer1", "CN=issuer1", "000", "key000");
  CERTCertificate notEvictedSubject;
  MakeFakeCert(&notEvictedSubject, "CN=subject0000", "CN=issuer1", "0000", "key000");
  PRTime timeIn = PR_Now();
  PutAndGet(cache, &notEvictedSubject, &issuer, SEC_ERROR_REVOKED_CERTIFICATE, timeIn);
  // By putting more distinct entries in the cache than it can hold,
  // we cause the least recently used entry that isn't revoked to be evicted.
  for (int i = 1; i < MaxCacheEntries + 1; i++) {
    CERTCertificate subject;
    char subjectBuf[64];
    PR_snprintf(subjectBuf, sizeof(subjectBuf), "CN=subject%04d", i);
    char serialBuf[8];
    PR_snprintf(serialBuf, sizeof(serialBuf), "%04d", i);
    MakeFakeCert(&subject, subjectBuf, "CN=issuer1", serialBuf, "key000");
    PutAndGet(cache, &subject, &issuer, 0, timeIn + i);
  }
  PRErrorCode errorOut;
  PRTime timeOut;
  ASSERT_TRUE(cache.Get(&notEvictedSubject, &issuer, errorOut, timeOut));
  ASSERT_TRUE(errorOut == SEC_ERROR_REVOKED_CERTIFICATE && timeOut == timeIn);
  CERTCertificate evictedSubject;
  MakeFakeCert(&evictedSubject, "CN=subject0001", "CN=issuer1", "0001", "key000");
  ASSERT_FALSE(cache.Get(&evictedSubject, &issuer, errorOut, timeOut));
}

TEST_F(OCSPCacheTest, TestEverythingIsRevoked)
{
  SCOPED_TRACE("");
  CERTCertificate issuer;
  MakeFakeCert(&issuer, "CN=issuer1", "CN=issuer1", "000", "key000");
  PRTime timeIn = PR_Now();
  // Fill up the cache with revoked responses.
  for (int i = 0; i < MaxCacheEntries; i++) {
    CERTCertificate subject;
    char subjectBuf[64];
    PR_snprintf(subjectBuf, sizeof(subjectBuf), "CN=subject%04d", i);
    char serialBuf[8];
    PR_snprintf(serialBuf, sizeof(serialBuf), "%04d", i);
    MakeFakeCert(&subject, subjectBuf, "CN=issuer1", serialBuf, "key000");
    PutAndGet(cache, &subject, &issuer, SEC_ERROR_REVOKED_CERTIFICATE, timeIn + i);
  }
  CERTCertificate goodSubject;
  MakeFakeCert(&goodSubject, "CN=subject1025", "CN=issuer1", "1025", "key000");
  // This will "succeed", allowing verification to continue. However,
  // nothing was actually put in the cache.
  SECStatus result = cache.Put(&goodSubject, &issuer, 0, timeIn + 1025 - 50,
                               timeIn + 1025);
  ASSERT_TRUE(result == SECSuccess);
  PRErrorCode errorOut;
  PRTime timeOut;
  ASSERT_FALSE(cache.Get(&goodSubject, &issuer, errorOut, timeOut));

  CERTCertificate revokedSubject;
  MakeFakeCert(&revokedSubject, "CN=subject1026", "CN=issuer1", "1026", "key000");
  // This will fail, causing verification to fail.
  result = cache.Put(&revokedSubject, &issuer, SEC_ERROR_REVOKED_CERTIFICATE,
                     timeIn + 1026 - 50, timeIn + 1026);
  PRErrorCode error = PR_GetError();
  ASSERT_TRUE(result == SECFailure);
  ASSERT_TRUE(error == SEC_ERROR_REVOKED_CERTIFICATE);
}

TEST_F(OCSPCacheTest, VariousIssuers)
{
  SCOPED_TRACE("");
  CERTCertificate issuer1;
  MakeFakeCert(&issuer1, "CN=issuer1", "CN=issuer1", "000", "key000");
  CERTCertificate issuer2;
  MakeFakeCert(&issuer2, "CN=issuer2", "CN=issuer2", "000", "key001");
  CERTCertificate issuer3;
  // Note: same CN as issuer1
  MakeFakeCert(&issuer3, "CN=issuer1", "CN=issuer3", "000", "key003");
  CERTCertificate subject;
  MakeFakeCert(&subject, "CN=subject", "CN=issuer1", "001", "key002");
  PRTime timeIn = PR_Now();
  PutAndGet(cache, &subject, &issuer1, 0, timeIn);
  PRErrorCode errorOut;
  PRTime timeOut;
  ASSERT_TRUE(cache.Get(&subject, &issuer1, errorOut, timeOut));
  ASSERT_TRUE(errorOut == 0 && timeOut == timeIn);
  ASSERT_FALSE(cache.Get(&subject, &issuer2, errorOut, timeOut));
  ASSERT_FALSE(cache.Get(&subject, &issuer3, errorOut, timeOut));
}

TEST_F(OCSPCacheTest, Times)
{
  SCOPED_TRACE("");
  CERTCertificate subject;
  MakeFakeCert(&subject, "CN=subject1", "CN=issuer1", "001", "key001");
  CERTCertificate issuer;
  MakeFakeCert(&issuer, "CN=issuer1", "CN=issuer1", "000", "key000");
  PutAndGet(cache, &subject, &issuer, SEC_ERROR_OCSP_UNKNOWN_CERT, 100);
  PutAndGet(cache, &subject, &issuer, 0, 200);
  // This should not override the more recent entry.
  SECStatus rv = cache.Put(&subject, &issuer, SEC_ERROR_OCSP_UNKNOWN_CERT, 100, 100);
  ASSERT_TRUE(rv == SECSuccess);
  PRErrorCode errorOut;
  PRTime timeOut;
  ASSERT_TRUE(cache.Get(&subject, &issuer, errorOut, timeOut));
  // Here we see the more recent time.
  ASSERT_TRUE(errorOut == 0 && timeOut == 200);

  // SEC_ERROR_REVOKED_CERTIFICATE overrides everything
  PutAndGet(cache, &subject, &issuer, SEC_ERROR_REVOKED_CERTIFICATE, 50);
}

TEST_F(OCSPCacheTest, NetworkFailure)
{
  SCOPED_TRACE("");
  CERTCertificate subject;
  MakeFakeCert(&subject, "CN=subject1", "CN=issuer1", "001", "key001");
  CERTCertificate issuer;
  MakeFakeCert(&issuer, "CN=issuer1", "CN=issuer1", "000", "key000");
  PutAndGet(cache, &subject, &issuer, PR_CONNECT_REFUSED_ERROR, 100);
  PutAndGet(cache, &subject, &issuer, 0, 200);
  // This should not override the already present entry.
  SECStatus rv = cache.Put(&subject, &issuer, PR_CONNECT_REFUSED_ERROR, 300, 350);
  ASSERT_TRUE(rv == SECSuccess);
  PRErrorCode errorOut;
  PRTime timeOut;
  ASSERT_TRUE(cache.Get(&subject, &issuer, errorOut, timeOut));
  ASSERT_TRUE(errorOut == 0 && timeOut == 200);

  PutAndGet(cache, &subject, &issuer, SEC_ERROR_OCSP_UNKNOWN_CERT, 400);
  // This should not override the already present entry.
  rv = cache.Put(&subject, &issuer, PR_CONNECT_REFUSED_ERROR, 500, 550);
  ASSERT_TRUE(rv == SECSuccess);
  ASSERT_TRUE(cache.Get(&subject, &issuer, errorOut, timeOut));
  ASSERT_TRUE(errorOut == SEC_ERROR_OCSP_UNKNOWN_CERT && timeOut == 400);

  PutAndGet(cache, &subject, &issuer, SEC_ERROR_REVOKED_CERTIFICATE, 600);
  // This should not override the already present entry.
  rv = cache.Put(&subject, &issuer, PR_CONNECT_REFUSED_ERROR, 700, 750);
  ASSERT_TRUE(rv == SECSuccess);
  ASSERT_TRUE(cache.Get(&subject, &issuer, errorOut, timeOut));
  ASSERT_TRUE(errorOut == SEC_ERROR_REVOKED_CERTIFICATE && timeOut == 600);
}
