/* 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/. */

#ifndef PKIM_H
#include "pkim.h"
#endif /* PKIM_H */

#ifndef PKIT_H
#include "pkit.h"
#endif /* PKIT_H */

#ifndef NSSPKI_H
#include "nsspki.h"
#endif /* NSSPKI_H */

#ifndef PKI_H
#include "pki.h"
#endif /* PKI_H */

#ifndef NSSBASE_H
#include "nssbase.h"
#endif /* NSSBASE_H */

#ifndef BASE_H
#include "base.h"
#endif /* BASE_H */

#include "cert.h"
#include "dev.h"
#include "pki3hack.h"

#ifdef DEBUG_CACHE
static PRLogModuleInfo *s_log = NULL;
#endif

#ifdef DEBUG_CACHE
static void
log_item_dump(const char *msg, NSSItem *it)
{
    char buf[33];
    int i, j;
    for (i = 0; i < 10 && i < it->size; i++) {
        snprintf(&buf[2 * i], sizeof(buf) - 2 * i, "%02X", ((PRUint8 *)it->data)[i]);
    }
    if (it->size > 10) {
        snprintf(&buf[2 * i], sizeof(buf) - 2 * i, "..");
        i += 1;
        for (j = it->size - 1; i <= 16 && j > 10; i++, j--) {
            snprintf(&buf[2 * i], sizeof(buf) - 2 * i, "%02X", ((PRUint8 *)it->data)[j]);
        }
    }
    PR_LOG(s_log, PR_LOG_DEBUG, ("%s: %s", msg, buf));
}
#endif

#ifdef DEBUG_CACHE
static void
log_cert_ref(const char *msg, NSSCertificate *c)
{
    PR_LOG(s_log, PR_LOG_DEBUG, ("%s: %s", msg, (c->nickname) ? c->nickname : c->email));
    log_item_dump("\tserial", &c->serial);
    log_item_dump("\tsubject", &c->subject);
}
#endif

/* Certificate cache routines */

/* XXX
 * Locking is not handled well at all.  A single, global lock with sub-locks
 * in the collection types.  Cleanup needed.
 */

/* should it live in its own arena? */
struct nssTDCertificateCacheStr {
    PRLock *lock; /* Must not be held when calling nssSlot_IsTokenPresent. See bug 1625791. */
    NSSArena *arena;
    nssHash *issuerAndSN;
    nssHash *subject;
    nssHash *nickname;
    nssHash *email;
};

struct cache_entry_str {
    union {
        NSSCertificate *cert;
        nssList *list;
        void *value;
    } entry;
    PRUint32 hits;
    PRTime lastHit;
    NSSArena *arena;
    NSSUTF8 *nickname;
    NSSASCII7 *email;
};

typedef struct cache_entry_str cache_entry;

static cache_entry *
new_cache_entry(NSSArena *arena, void *value, PRBool ownArena)
{
    cache_entry *ce = nss_ZNEW(arena, cache_entry);
    if (ce) {
        ce->entry.value = value;
        ce->hits = 1;
        ce->lastHit = PR_Now();
        if (ownArena) {
            ce->arena = arena;
        }
        ce->nickname = NULL;
    }
    return ce;
}

/* this should not be exposed in a header, but is here to keep the above
 * types/functions static
 */
NSS_IMPLEMENT PRStatus
nssTrustDomain_InitializeCache(
    NSSTrustDomain *td,
    PRUint32 cacheSize)
{
    NSSArena *arena;
    nssTDCertificateCache *cache = td->cache;
#ifdef DEBUG_CACHE
    s_log = PR_NewLogModule("nss_cache");
    PR_ASSERT(s_log);
#endif
    PR_ASSERT(!cache);
    arena = nssArena_Create();
    if (!arena) {
        return PR_FAILURE;
    }
    cache = nss_ZNEW(arena, nssTDCertificateCache);
    if (!cache) {
        nssArena_Destroy(arena);
        return PR_FAILURE;
    }
    cache->lock = PR_NewLock();
    if (!cache->lock) {
        nssArena_Destroy(arena);
        return PR_FAILURE;
    }
    /* Create the issuer and serial DER --> certificate hash */
    cache->issuerAndSN = nssHash_CreateCertificate(arena, cacheSize);
    if (!cache->issuerAndSN) {
        goto loser;
    }
    /* Create the subject DER --> subject list hash */
    cache->subject = nssHash_CreateItem(arena, cacheSize);
    if (!cache->subject) {
        goto loser;
    }
    /* Create the nickname --> subject list hash */
    cache->nickname = nssHash_CreateString(arena, cacheSize);
    if (!cache->nickname) {
        goto loser;
    }
    /* Create the email --> list of subject lists hash */
    cache->email = nssHash_CreateString(arena, cacheSize);
    if (!cache->email) {
        goto loser;
    }
    cache->arena = arena;
    td->cache = cache;
#ifdef DEBUG_CACHE
    PR_LOG(s_log, PR_LOG_DEBUG, ("Cache initialized."));
#endif
    return PR_SUCCESS;
loser:
    PR_DestroyLock(cache->lock);
    nssArena_Destroy(arena);
    td->cache = NULL;
#ifdef DEBUG_CACHE
    PR_LOG(s_log, PR_LOG_DEBUG, ("Cache initialization failed."));
#endif
    return PR_FAILURE;
}

/* The entries of the hashtable are currently dependent on the certificate(s)
 * that produced them.  That is, the entries will be freed when the cert is
 * released from the cache.  If there are certs in the cache at any time,
 * including shutdown, the hash table entries will hold memory.  In order for
 * clean shutdown, it is necessary for there to be no certs in the cache.
 */

extern const NSSError NSS_ERROR_INTERNAL_ERROR;
extern const NSSError NSS_ERROR_BUSY;

NSS_IMPLEMENT PRStatus
nssTrustDomain_DestroyCache(NSSTrustDomain *td)
{
    if (!td->cache) {
        nss_SetError(NSS_ERROR_INTERNAL_ERROR);
        return PR_FAILURE;
    }
    if (nssHash_Count(td->cache->issuerAndSN) > 0) {
        nss_SetError(NSS_ERROR_BUSY);
        return PR_FAILURE;
    }
    PR_DestroyLock(td->cache->lock);
    nssHash_Destroy(td->cache->issuerAndSN);
    nssHash_Destroy(td->cache->subject);
    nssHash_Destroy(td->cache->nickname);
    nssHash_Destroy(td->cache->email);
    nssArena_Destroy(td->cache->arena);
    td->cache = NULL;
#ifdef DEBUG_CACHE
    PR_LOG(s_log, PR_LOG_DEBUG, ("Cache destroyed."));
#endif
    return PR_SUCCESS;
}

static PRStatus
remove_issuer_and_serial_entry(
    nssTDCertificateCache *cache,
    NSSCertificate *cert)
{
    /* Remove the cert from the issuer/serial hash */
    nssHash_Remove(cache->issuerAndSN, cert);
#ifdef DEBUG_CACHE
    log_cert_ref("removed issuer/sn", cert);
#endif
    return PR_SUCCESS;
}

static PRStatus
remove_subject_entry(
    nssTDCertificateCache *cache,
    NSSCertificate *cert,
    nssList **subjectList,
    NSSUTF8 **nickname,
    NSSASCII7 **email,
    NSSArena **arena)
{
    PRStatus nssrv;
    cache_entry *ce;
    *subjectList = NULL;
    *arena = NULL;
    /* Get the subject list for the cert's subject */
    ce = (cache_entry *)nssHash_Lookup(cache->subject, &cert->subject);
    if (ce) {
        /* Remove the cert from the subject hash */
        nssList_Remove(ce->entry.list, cert);
        *subjectList = ce->entry.list;
        *nickname = ce->nickname;
        *email = ce->email;
        *arena = ce->arena;
        nssrv = PR_SUCCESS;
#ifdef DEBUG_CACHE
        log_cert_ref("removed cert", cert);
        log_item_dump("from subject list", &cert->subject);
#endif
    } else {
        nssrv = PR_FAILURE;
    }
    return nssrv;
}

static PRStatus
remove_nickname_entry(
    nssTDCertificateCache *cache,
    NSSUTF8 *nickname,
    nssList *subjectList)
{
    PRStatus nssrv;
    if (nickname) {
        nssHash_Remove(cache->nickname, nickname);
        nssrv = PR_SUCCESS;
#ifdef DEBUG_CACHE
        PR_LOG(s_log, PR_LOG_DEBUG, ("removed nickname %s", nickname));
#endif
    } else {
        nssrv = PR_FAILURE;
    }
    return nssrv;
}

static PRStatus
remove_email_entry(
    nssTDCertificateCache *cache,
    NSSASCII7 *email,
    nssList *subjectList)
{
    PRStatus nssrv = PR_FAILURE;
    cache_entry *ce;
    if (email) {
        ce = (cache_entry *)nssHash_Lookup(cache->email, email);
        if (ce) {
            nssList *subjects = ce->entry.list;
            /* Remove the subject list from the email hash */
            if (subjects) {
                nssList_Remove(subjects, subjectList);
#ifdef DEBUG_CACHE
                PR_LOG(s_log, PR_LOG_DEBUG,
                       ("removed subject list for email %s", email));
#endif
                if (nssList_Count(subjects) == 0) {
                    /* No more subject lists for email, delete list and
                     * remove hash entry
                     */
                    (void)nssList_Destroy(subjects);
                    nssHash_Remove(cache->email, email);
                    /* there are no entries left for this address, free space
                     * used for email entries
                     */
                    nssArena_Destroy(ce->arena);
#ifdef DEBUG_CACHE
                    PR_LOG(s_log, PR_LOG_DEBUG, ("removed email %s", email));
#endif
                }
            }
            nssrv = PR_SUCCESS;
        }
    }
    return nssrv;
}

NSS_IMPLEMENT void
nssTrustDomain_RemoveCertFromCacheLOCKED(
    NSSTrustDomain *td,
    NSSCertificate *cert)
{
    nssList *subjectList;
    cache_entry *ce;
    NSSArena *arena;
    NSSUTF8 *nickname = NULL;
    NSSASCII7 *email = NULL;

#ifdef DEBUG_CACHE
    log_cert_ref("attempt to remove cert", cert);
#endif
    ce = (cache_entry *)nssHash_Lookup(td->cache->issuerAndSN, cert);
    if (!ce || ce->entry.cert != cert) {
/* If it's not in the cache, or a different cert is (this is really
 * for safety reasons, though it shouldn't happen), do nothing
 */
#ifdef DEBUG_CACHE
        PR_LOG(s_log, PR_LOG_DEBUG, ("but it wasn't in the cache"));
#endif
        return;
    }
    (void)remove_issuer_and_serial_entry(td->cache, cert);
    (void)remove_subject_entry(td->cache, cert, &subjectList,
                               &nickname, &email, &arena);
    if (nssList_Count(subjectList) == 0) {
        (void)remove_nickname_entry(td->cache, nickname, subjectList);
        (void)remove_email_entry(td->cache, email, subjectList);
        (void)nssList_Destroy(subjectList);
        nssHash_Remove(td->cache->subject, &cert->subject);
        /* there are no entries left for this subject, free the space used
         * for both the nickname and subject entries
         */
        if (arena) {
            nssArena_Destroy(arena);
        }
    }
}

NSS_IMPLEMENT void
nssTrustDomain_LockCertCache(NSSTrustDomain *td)
{
    PR_Lock(td->cache->lock);
}

NSS_IMPLEMENT void
nssTrustDomain_UnlockCertCache(NSSTrustDomain *td)
{
    PR_Unlock(td->cache->lock);
}

struct token_cert_dtor {
    NSSToken *token;
    nssTDCertificateCache *cache;
    NSSCertificate **certs;
    PRUint32 numCerts, arrSize;
};

static void
remove_token_certs(const void *k, void *v, void *a)
{
    NSSCertificate *c = (NSSCertificate *)k;
    nssPKIObject *object = &c->object;
    struct token_cert_dtor *dtor = a;
    PRUint32 i;
    nssPKIObject_AddRef(object);
    nssPKIObject_Lock(object);
    for (i = 0; i < object->numInstances; i++) {
        if (object->instances[i]->token == dtor->token) {
            nssCryptokiObject_Destroy(object->instances[i]);
            object->instances[i] = object->instances[object->numInstances - 1];
            object->instances[object->numInstances - 1] = NULL;
            object->numInstances--;
            dtor->certs[dtor->numCerts++] = c;
            if (dtor->numCerts == dtor->arrSize) {
                dtor->arrSize *= 2;
                dtor->certs = nss_ZREALLOCARRAY(dtor->certs,
                                                NSSCertificate *,
                                                dtor->arrSize);
            }
            break;
        }
    }
    nssPKIObject_Unlock(object);
    nssPKIObject_Destroy(object);
    return;
}

/*
 * Remove all certs for the given token from the cache.  This is
 * needed if the token is removed.
 */
NSS_IMPLEMENT PRStatus
nssTrustDomain_RemoveTokenCertsFromCache(
    NSSTrustDomain *td,
    NSSToken *token)
{
    NSSCertificate **certs;
    PRUint32 i, arrSize = 10;
    struct token_cert_dtor dtor;
    certs = nss_ZNEWARRAY(NULL, NSSCertificate *, arrSize);
    if (!certs) {
        return PR_FAILURE;
    }
    dtor.cache = td->cache;
    dtor.token = token;
    dtor.certs = certs;
    dtor.numCerts = 0;
    dtor.arrSize = arrSize;
    PR_Lock(td->cache->lock);
    nssHash_Iterate(td->cache->issuerAndSN, remove_token_certs, &dtor);
    for (i = 0; i < dtor.numCerts; i++) {
        if (dtor.certs[i]->object.numInstances == 0) {
            nssTrustDomain_RemoveCertFromCacheLOCKED(td, dtor.certs[i]);
            dtor.certs[i] = NULL; /* skip this cert in the second for loop */
        } else {
            /* make sure it doesn't disappear on us before we finish */
            nssCertificate_AddRef(dtor.certs[i]);
        }
    }
    PR_Unlock(td->cache->lock);
    for (i = 0; i < dtor.numCerts; i++) {
        if (dtor.certs[i]) {
            STAN_ForceCERTCertificateUpdate(dtor.certs[i]);
            nssCertificate_Destroy(dtor.certs[i]);
        }
    }
    nss_ZFreeIf(dtor.certs);
    return PR_SUCCESS;
}

NSS_IMPLEMENT PRStatus
nssTrustDomain_UpdateCachedTokenCerts(
    NSSTrustDomain *td,
    NSSToken *token)
{
    NSSCertificate **cp, **cached = NULL;
    nssList *certList;
    PRUint32 count;
    certList = nssList_Create(NULL, PR_FALSE);
    if (!certList)
        return PR_FAILURE;
    (void)nssTrustDomain_GetCertsFromCache(td, certList);
    count = nssList_Count(certList);
    if (count > 0) {
        cached = nss_ZNEWARRAY(NULL, NSSCertificate *, count + 1);
        if (!cached) {
            nssList_Destroy(certList);
            return PR_FAILURE;
        }
        nssList_GetArray(certList, (void **)cached, count);
        for (cp = cached; *cp; cp++) {
            nssCryptokiObject *instance;
            NSSCertificate *c = *cp;
            nssTokenSearchType tokenOnly = nssTokenSearchType_TokenOnly;
            instance = nssToken_FindCertificateByIssuerAndSerialNumber(
                token,
                NULL,
                &c->issuer,
                &c->serial,
                tokenOnly,
                NULL);
            if (instance) {
                nssPKIObject_AddInstance(&c->object, instance);
                STAN_ForceCERTCertificateUpdate(c);
            }
        }
        nssCertificateArray_Destroy(cached);
    }
    nssList_Destroy(certList);
    return PR_SUCCESS;
}

static PRStatus
add_issuer_and_serial_entry(
    NSSArena *arena,
    nssTDCertificateCache *cache,
    NSSCertificate *cert)
{
    cache_entry *ce;
    ce = new_cache_entry(arena, (void *)cert, PR_FALSE);
#ifdef DEBUG_CACHE
    log_cert_ref("added to issuer/sn", cert);
#endif
    return nssHash_Add(cache->issuerAndSN, cert, (void *)ce);
}

static PRStatus
add_subject_entry(
    NSSArena *arena,
    nssTDCertificateCache *cache,
    NSSCertificate *cert,
    NSSUTF8 *nickname,
    nssList **subjectList)
{
    PRStatus nssrv;
    nssList *list;
    cache_entry *ce;
    *subjectList = NULL; /* this is only set if a new one is created */
    ce = (cache_entry *)nssHash_Lookup(cache->subject, &cert->subject);
    if (ce) {
        ce->hits++;
        ce->lastHit = PR_Now();
        /* The subject is already in, add this cert to the list */
        nssrv = nssList_AddUnique(ce->entry.list, cert);
#ifdef DEBUG_CACHE
        log_cert_ref("added to existing subject list", cert);
#endif
    } else {
        NSSDER *subject;
        /* Create a new subject list for the subject */
        list = nssList_Create(arena, PR_FALSE);
        if (!list) {
            return PR_FAILURE;
        }
        ce = new_cache_entry(arena, (void *)list, PR_TRUE);
        if (!ce) {
            return PR_FAILURE;
        }
        if (nickname) {
            ce->nickname = nssUTF8_Duplicate(nickname, arena);
        }
        if (cert->email) {
            ce->email = nssUTF8_Duplicate(cert->email, arena);
        }
        nssList_SetSortFunction(list, nssCertificate_SubjectListSort);
        /* Add the cert entry to this list of subjects */
        nssrv = nssList_AddUnique(list, cert);
        if (nssrv != PR_SUCCESS) {
            return nssrv;
        }
        /* Add the subject list to the cache */
        subject = nssItem_Duplicate(&cert->subject, arena, NULL);
        if (!subject) {
            return PR_FAILURE;
        }
        nssrv = nssHash_Add(cache->subject, subject, ce);
        if (nssrv != PR_SUCCESS) {
            return nssrv;
        }
        *subjectList = list;
#ifdef DEBUG_CACHE
        log_cert_ref("created subject list", cert);
#endif
    }
    return nssrv;
}

static PRStatus
add_nickname_entry(
    NSSArena *arena,
    nssTDCertificateCache *cache,
    NSSUTF8 *certNickname,
    nssList *subjectList)
{
    PRStatus nssrv = PR_SUCCESS;
    cache_entry *ce;
    ce = (cache_entry *)nssHash_Lookup(cache->nickname, certNickname);
    if (ce) {
        /* This is a collision.  A nickname entry already exists for this
         * subject, but a subject entry didn't.  This would imply there are
         * two subjects using the same nickname, which is not allowed.
         */
        return PR_FAILURE;
    } else {
        NSSUTF8 *nickname;
        ce = new_cache_entry(arena, subjectList, PR_FALSE);
        if (!ce) {
            return PR_FAILURE;
        }
        nickname = nssUTF8_Duplicate(certNickname, arena);
        if (!nickname) {
            return PR_FAILURE;
        }
        nssrv = nssHash_Add(cache->nickname, nickname, ce);
#ifdef DEBUG_CACHE
        log_cert_ref("created nickname for", cert);
#endif
    }
    return nssrv;
}

static PRStatus
add_email_entry(
    nssTDCertificateCache *cache,
    NSSCertificate *cert,
    nssList *subjectList)
{
    PRStatus nssrv = PR_SUCCESS;
    nssList *subjects;
    cache_entry *ce;
    ce = (cache_entry *)nssHash_Lookup(cache->email, cert->email);
    if (ce) {
        /* Already have an entry for this email address, but not subject */
        subjects = ce->entry.list;
        nssrv = nssList_AddUnique(subjects, subjectList);
        ce->hits++;
        ce->lastHit = PR_Now();
#ifdef DEBUG_CACHE
        log_cert_ref("added subject to email for", cert);
#endif
    } else {
        NSSASCII7 *email;
        NSSArena *arena;
        arena = nssArena_Create();
        if (!arena) {
            return PR_FAILURE;
        }
        /* Create a new list of subject lists, add this subject */
        subjects = nssList_Create(arena, PR_TRUE);
        if (!subjects) {
            nssArena_Destroy(arena);
            return PR_FAILURE;
        }
        /* Add the new subject to the list */
        nssrv = nssList_AddUnique(subjects, subjectList);
        if (nssrv != PR_SUCCESS) {
            nssArena_Destroy(arena);
            return nssrv;
        }
        /* Add the new entry to the cache */
        ce = new_cache_entry(arena, (void *)subjects, PR_TRUE);
        if (!ce) {
            nssArena_Destroy(arena);
            return PR_FAILURE;
        }
        email = nssUTF8_Duplicate(cert->email, arena);
        if (!email) {
            nssArena_Destroy(arena);
            return PR_FAILURE;
        }
        nssrv = nssHash_Add(cache->email, email, ce);
        if (nssrv != PR_SUCCESS) {
            nssArena_Destroy(arena);
            return nssrv;
        }
#ifdef DEBUG_CACHE
        log_cert_ref("created email for", cert);
#endif
    }
    return nssrv;
}

extern const NSSError NSS_ERROR_CERTIFICATE_IN_CACHE;

static void
remove_object_instances(
    nssPKIObject *object,
    nssCryptokiObject **instances,
    int numInstances)
{
    int i;

    for (i = 0; i < numInstances; i++) {
        nssPKIObject_RemoveInstanceForToken(object, instances[i]->token);
    }
}

static SECStatus
merge_object_instances(
    nssPKIObject *to,
    nssPKIObject *from)
{
    nssCryptokiObject **instances, **ci;
    int i;
    SECStatus rv = SECSuccess;

    instances = nssPKIObject_GetInstances(from);
    if (instances == NULL) {
        return SECFailure;
    }
    for (ci = instances, i = 0; *ci; ci++, i++) {
        nssCryptokiObject *instance = nssCryptokiObject_Clone(*ci);
        if (instance) {
            if (nssPKIObject_AddInstance(to, instance) == PR_SUCCESS) {
                continue;
            }
            nssCryptokiObject_Destroy(instance);
        }
        remove_object_instances(to, instances, i);
        rv = SECFailure;
        break;
    }
    nssCryptokiObjectArray_Destroy(instances);
    return rv;
}

NSS_IMPLEMENT NSSCertificate *
nssTrustDomain_AddCertToCache(
    NSSTrustDomain *td,
    NSSCertificate *cert)
{
    NSSArena *arena = NULL;
    nssList *subjectList = NULL;
    PRStatus nssrv;
    PRUint32 added = 0;
    cache_entry *ce;
    NSSCertificate *rvCert = NULL;
    NSSASCII7 *email = NULL;
    NSSUTF8 *certNickname = nssCertificate_GetNickname(cert, NULL);

    /* Set cc->trust and cc->nssCertificate before taking td->cache->lock.
     * Otherwise, the sorter in add_subject_entry may eventually call
     * nssSlot_IsTokenPresent, which must not occur while the cache lock
     * is held. See bugs 1625791 and 1651564 for details. */
    if (cert->type == NSSCertificateType_PKIX) {
        (void)STAN_GetCERTCertificate(cert);
    }

    PR_Lock(td->cache->lock);
    /* If it exists in the issuer/serial hash, it's already in all */
    ce = (cache_entry *)nssHash_Lookup(td->cache->issuerAndSN, cert);
    if (ce) {
        ce->hits++;
        ce->lastHit = PR_Now();
        rvCert = nssCertificate_AddRef(ce->entry.cert);
#ifdef DEBUG_CACHE
        log_cert_ref("attempted to add cert already in cache", cert);
#endif
        PR_Unlock(td->cache->lock);
        nss_ZFreeIf(certNickname);
        /* collision - somebody else already added the cert
         * to the cache before this thread got around to it.
         */
        /* merge the instances of the cert */
        if (merge_object_instances(&rvCert->object, &cert->object) != SECSuccess) {
            nssCertificate_Destroy(rvCert);
            nssCertificate_Destroy(cert); // consume the owned reference to cert
            return NULL;
        }
        STAN_ForceCERTCertificateUpdate(rvCert);
        nssCertificate_Destroy(cert); // consume the owned reference to cert
        return rvCert;
    }
    /* create a new cache entry for this cert within the cert's arena*/
    nssrv = add_issuer_and_serial_entry(cert->object.arena, td->cache, cert);
    if (nssrv != PR_SUCCESS) {
        goto loser;
    }
    added++;
    /* create an arena for the nickname and subject entries */
    arena = nssArena_Create();
    if (!arena) {
        goto loser;
    }
    /* create a new subject list for this cert, or add to existing */
    nssrv = add_subject_entry(arena, td->cache, cert,
                              certNickname, &subjectList);
    if (nssrv != PR_SUCCESS) {
        goto loser;
    }
    added++;
    /* If a new subject entry was created, also need nickname and/or email */
    if (subjectList != NULL) {
        if (certNickname) {
            nssrv = add_nickname_entry(arena, td->cache,
                                       certNickname, subjectList);
            if (nssrv != PR_SUCCESS) {
                goto loser;
            }
            added++;
        }
        if (cert->email) {
            nssrv = add_email_entry(td->cache, cert, subjectList);
            if (nssrv != PR_SUCCESS) {
                goto loser;
            }
            added += 2;
        }
    } else {
        /* A new subject entry was not created.  arena is unused. */
        nssArena_Destroy(arena);
    }
    rvCert = cert; // rvCert is now the owned reference to cert
    PR_Unlock(td->cache->lock);
    nss_ZFreeIf(certNickname);
    return rvCert;
loser:
    nss_ZFreeIf(certNickname);
    certNickname = NULL;
    /* Remove any handles that have been created */
    subjectList = NULL;
    if (added >= 1) {
        (void)remove_issuer_and_serial_entry(td->cache, cert);
    }
    if (added >= 2) {
        (void)remove_subject_entry(td->cache, cert, &subjectList,
                                   &certNickname, &email, &arena);
    }
    if (added == 3 || added == 5) {
        (void)remove_nickname_entry(td->cache, certNickname, subjectList);
    }
    if (added >= 4) {
        (void)remove_email_entry(td->cache, email, subjectList);
    }
    if (subjectList) {
        nssHash_Remove(td->cache->subject, &cert->subject);
        nssList_Destroy(subjectList);
    }
    if (arena) {
        nssArena_Destroy(arena);
    }
    PR_Unlock(td->cache->lock);
    nssCertificate_Destroy(cert); // consume the owned reference to cert
    return NULL;
}

static NSSCertificate **
collect_subject_certs(
    nssList *subjectList,
    nssList *rvCertListOpt)
{
    NSSCertificate *c;
    NSSCertificate **rvArray = NULL;
    PRUint32 count;
    nssCertificateList_AddReferences(subjectList);
    if (rvCertListOpt) {
        nssListIterator *iter = nssList_CreateIterator(subjectList);
        if (!iter) {
            return (NSSCertificate **)NULL;
        }
        for (c = (NSSCertificate *)nssListIterator_Start(iter);
             c != (NSSCertificate *)NULL;
             c = (NSSCertificate *)nssListIterator_Next(iter)) {
            nssList_Add(rvCertListOpt, c);
        }
        nssListIterator_Finish(iter);
        nssListIterator_Destroy(iter);
    } else {
        count = nssList_Count(subjectList);
        rvArray = nss_ZNEWARRAY(NULL, NSSCertificate *, count + 1);
        if (!rvArray) {
            return (NSSCertificate **)NULL;
        }
        nssList_GetArray(subjectList, (void **)rvArray, count);
    }
    return rvArray;
}

/*
 * Find all cached certs with this subject.
 */
NSS_IMPLEMENT NSSCertificate **
nssTrustDomain_GetCertsForSubjectFromCache(
    NSSTrustDomain *td,
    NSSDER *subject,
    nssList *certListOpt)
{
    NSSCertificate **rvArray = NULL;
    cache_entry *ce;
#ifdef DEBUG_CACHE
    log_item_dump("looking for cert by subject", subject);
#endif
    PR_Lock(td->cache->lock);
    ce = (cache_entry *)nssHash_Lookup(td->cache->subject, subject);
    if (ce) {
        ce->hits++;
        ce->lastHit = PR_Now();
#ifdef DEBUG_CACHE
        PR_LOG(s_log, PR_LOG_DEBUG, ("... found, %d hits", ce->hits));
#endif
        rvArray = collect_subject_certs(ce->entry.list, certListOpt);
    }
    PR_Unlock(td->cache->lock);
    return rvArray;
}

/*
 * Find all cached certs with this label.
 */
NSS_IMPLEMENT NSSCertificate **
nssTrustDomain_GetCertsForNicknameFromCache(
    NSSTrustDomain *td,
    const NSSUTF8 *nickname,
    nssList *certListOpt)
{
    NSSCertificate **rvArray = NULL;
    cache_entry *ce;
#ifdef DEBUG_CACHE
    PR_LOG(s_log, PR_LOG_DEBUG, ("looking for cert by nick %s", nickname));
#endif
    PR_Lock(td->cache->lock);
    ce = (cache_entry *)nssHash_Lookup(td->cache->nickname, nickname);
    if (ce) {
        ce->hits++;
        ce->lastHit = PR_Now();
#ifdef DEBUG_CACHE
        PR_LOG(s_log, PR_LOG_DEBUG, ("... found, %d hits", ce->hits));
#endif
        rvArray = collect_subject_certs(ce->entry.list, certListOpt);
    }
    PR_Unlock(td->cache->lock);
    return rvArray;
}

/*
 * Find all cached certs with this email address.
 */
NSS_IMPLEMENT NSSCertificate **
nssTrustDomain_GetCertsForEmailAddressFromCache(
    NSSTrustDomain *td,
    NSSASCII7 *email,
    nssList *certListOpt)
{
    NSSCertificate **rvArray = NULL;
    cache_entry *ce;
    nssList *collectList = NULL;
    nssListIterator *iter = NULL;
    nssList *subjectList;
#ifdef DEBUG_CACHE
    PR_LOG(s_log, PR_LOG_DEBUG, ("looking for cert by email %s", email));
#endif
    PR_Lock(td->cache->lock);
    ce = (cache_entry *)nssHash_Lookup(td->cache->email, email);
    if (ce) {
        ce->hits++;
        ce->lastHit = PR_Now();
#ifdef DEBUG_CACHE
        PR_LOG(s_log, PR_LOG_DEBUG, ("... found, %d hits", ce->hits));
#endif
        /* loop over subject lists and get refs for certs */
        if (certListOpt) {
            collectList = certListOpt;
        } else {
            collectList = nssList_Create(NULL, PR_FALSE);
            if (!collectList) {
                PR_Unlock(td->cache->lock);
                return NULL;
            }
        }
        iter = nssList_CreateIterator(ce->entry.list);
        if (!iter) {
            PR_Unlock(td->cache->lock);
            if (!certListOpt) {
                nssList_Destroy(collectList);
            }
            return NULL;
        }
        for (subjectList = (nssList *)nssListIterator_Start(iter);
             subjectList != (nssList *)NULL;
             subjectList = (nssList *)nssListIterator_Next(iter)) {
            (void)collect_subject_certs(subjectList, collectList);
        }
        nssListIterator_Finish(iter);
        nssListIterator_Destroy(iter);
    }
    PR_Unlock(td->cache->lock);
    if (!certListOpt && collectList) {
        PRUint32 count = nssList_Count(collectList);
        rvArray = nss_ZNEWARRAY(NULL, NSSCertificate *, count);
        if (rvArray) {
            nssList_GetArray(collectList, (void **)rvArray, count);
        }
        nssList_Destroy(collectList);
    }
    return rvArray;
}

/*
 * Look for a specific cert in the cache
 */
NSS_IMPLEMENT NSSCertificate *
nssTrustDomain_GetCertForIssuerAndSNFromCache(
    NSSTrustDomain *td,
    NSSDER *issuer,
    NSSDER *serial)
{
    NSSCertificate certkey;
    NSSCertificate *rvCert = NULL;
    cache_entry *ce;
    certkey.issuer.data = issuer->data;
    certkey.issuer.size = issuer->size;
    certkey.serial.data = serial->data;
    certkey.serial.size = serial->size;
#ifdef DEBUG_CACHE
    log_item_dump("looking for cert by issuer/sn, issuer", issuer);
    log_item_dump("                               serial", serial);
#endif
    PR_Lock(td->cache->lock);
    ce = (cache_entry *)nssHash_Lookup(td->cache->issuerAndSN, &certkey);
    if (ce) {
        ce->hits++;
        ce->lastHit = PR_Now();
        rvCert = nssCertificate_AddRef(ce->entry.cert);
#ifdef DEBUG_CACHE
        PR_LOG(s_log, PR_LOG_DEBUG, ("... found, %d hits", ce->hits));
#endif
    }
    PR_Unlock(td->cache->lock);
    return rvCert;
}

/*
 * Look for a specific cert in the cache
 */
NSS_IMPLEMENT NSSCertificate *
nssTrustDomain_GetCertByDERFromCache(
    NSSTrustDomain *td,
    NSSDER *der)
{
    PRStatus nssrv = PR_FAILURE;
    NSSDER issuer, serial;
    NSSCertificate *rvCert;
    nssrv = nssPKIX509_GetIssuerAndSerialFromDER(der, &issuer, &serial);
    if (nssrv != PR_SUCCESS) {
        return NULL;
    }
#ifdef DEBUG_CACHE
    log_item_dump("looking for cert by DER", der);
#endif
    rvCert = nssTrustDomain_GetCertForIssuerAndSNFromCache(td,
                                                           &issuer, &serial);
    PORT_Free(issuer.data);
    PORT_Free(serial.data);
    return rvCert;
}

static void
cert_iter(const void *k, void *v, void *a)
{
    nssList *certList = (nssList *)a;
    NSSCertificate *c = (NSSCertificate *)k;
    nssList_Add(certList, nssCertificate_AddRef(c));
}

NSS_EXTERN NSSCertificate **
nssTrustDomain_GetCertsFromCache(
    NSSTrustDomain *td,
    nssList *certListOpt)
{
    NSSCertificate **rvArray = NULL;
    nssList *certList;
    if (certListOpt) {
        certList = certListOpt;
    } else {
        certList = nssList_Create(NULL, PR_FALSE);
        if (!certList) {
            return NULL;
        }
    }
    PR_Lock(td->cache->lock);
    nssHash_Iterate(td->cache->issuerAndSN, cert_iter, (void *)certList);
    PR_Unlock(td->cache->lock);
    if (!certListOpt) {
        PRUint32 count = nssList_Count(certList);
        rvArray = nss_ZNEWARRAY(NULL, NSSCertificate *, count);
        nssList_GetArray(certList, (void **)rvArray, count);
        /* array takes the references */
        nssList_Destroy(certList);
    }
    return rvArray;
}

NSS_IMPLEMENT void
nssTrustDomain_DumpCacheInfo(
    NSSTrustDomain *td,
    void (*cert_dump_iter)(const void *, void *, void *),
    void *arg)
{
    PR_Lock(td->cache->lock);
    nssHash_Iterate(td->cache->issuerAndSN, cert_dump_iter, arg);
    PR_Unlock(td->cache->lock);
}
