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

/*
 * Stuff specific to S/MIME policy and interoperability.
 * Depends on PKCS7, but there should be no dependency the other way around.
 */

#include "secmime.h"
#include "secoid.h"
#include "pk11func.h"
#include "ciferfam.h" /* for CIPHER_FAMILY symbols */
#include "secasn1.h"
#include "secitem.h"
#include "cert.h"
#include "key.h"
#include "secerr.h"

typedef struct smime_cipher_map_struct {
    unsigned long cipher;
    SECOidTag algtag;
    SECItem *parms;
} smime_cipher_map;

/*
 * These are macros because I think some subsequent parameters,
 * like those for RC5, will want to use them, too, separately.
 */
#define SMIME_DER_INTVAL_16 SEC_ASN1_INTEGER, 0x01, 0x10
#define SMIME_DER_INTVAL_40 SEC_ASN1_INTEGER, 0x01, 0x28
#define SMIME_DER_INTVAL_64 SEC_ASN1_INTEGER, 0x01, 0x40
#define SMIME_DER_INTVAL_128 SEC_ASN1_INTEGER, 0x02, 0x00, 0x80

#ifdef SMIME_DOES_RC5 /* will be needed; quiet unused warning for now */
static unsigned char smime_int16[] = { SMIME_DER_INTVAL_16 };
#endif
static unsigned char smime_int40[] = { SMIME_DER_INTVAL_40 };
static unsigned char smime_int64[] = { SMIME_DER_INTVAL_64 };
static unsigned char smime_int128[] = { SMIME_DER_INTVAL_128 };

static SECItem smime_rc2p40 = { siBuffer, smime_int40, sizeof(smime_int40) };
static SECItem smime_rc2p64 = { siBuffer, smime_int64, sizeof(smime_int64) };
static SECItem smime_rc2p128 = { siBuffer, smime_int128, sizeof(smime_int128) };

static smime_cipher_map smime_cipher_maps[] = {
    { SMIME_RC2_CBC_40, SEC_OID_RC2_CBC, &smime_rc2p40 },
    { SMIME_RC2_CBC_64, SEC_OID_RC2_CBC, &smime_rc2p64 },
    { SMIME_RC2_CBC_128, SEC_OID_RC2_CBC, &smime_rc2p128 },
#ifdef SMIME_DOES_RC5
    { SMIME_RC5PAD_64_16_40, SEC_OID_RC5_CBC_PAD, &smime_rc5p40 },
    { SMIME_RC5PAD_64_16_64, SEC_OID_RC5_CBC_PAD, &smime_rc5p64 },
    { SMIME_RC5PAD_64_16_128, SEC_OID_RC5_CBC_PAD, &smime_rc5p128 },
#endif
    { SMIME_DES_CBC_56, SEC_OID_DES_CBC, NULL },
    { SMIME_DES_EDE3_168, SEC_OID_DES_EDE3_CBC, NULL }
};

/*
 * Note, the following value really just needs to be an upper bound
 * on the ciphers.
 */
static const int smime_symmetric_count = sizeof(smime_cipher_maps) / sizeof(smime_cipher_map);

static unsigned long *smime_prefs, *smime_newprefs;
static int smime_current_pref_index = 0;
static PRBool smime_prefs_complete = PR_FALSE;
static PRBool smime_prefs_changed = PR_TRUE;

static unsigned long smime_policy_bits = 0;

static int
smime_mapi_by_cipher(unsigned long cipher)
{
    int i;

    for (i = 0; i < smime_symmetric_count; i++) {
        if (smime_cipher_maps[i].cipher == cipher)
            break;
    }

    if (i == smime_symmetric_count)
        return -1;

    return i;
}

/*
 * this function locally records the user's preference
 */
SECStatus
SECMIME_EnableCipher(long which, int on)
{
    unsigned long mask;

    if (smime_newprefs == NULL || smime_prefs_complete) {
        /*
         * This is either the very first time, or we are starting over.
         */
        smime_newprefs = (unsigned long *)PORT_ZAlloc(smime_symmetric_count * sizeof(*smime_newprefs));
        if (smime_newprefs == NULL)
            return SECFailure;
        smime_current_pref_index = 0;
        smime_prefs_complete = PR_FALSE;
    }

    mask = which & CIPHER_FAMILYID_MASK;
    if (mask == CIPHER_FAMILYID_MASK) {
        /*
         * This call signifies that all preferences have been set.
         * Move "newprefs" over, after checking first whether or
         * not the new ones are different from the old ones.
         */
        if (smime_prefs != NULL) {
            if (PORT_Memcmp(smime_prefs, smime_newprefs,
                            smime_symmetric_count * sizeof(*smime_prefs)) == 0)
                smime_prefs_changed = PR_FALSE;
            else
                smime_prefs_changed = PR_TRUE;
            PORT_Free(smime_prefs);
        }

        smime_prefs = smime_newprefs;
        smime_prefs_complete = PR_TRUE;
        return SECSuccess;
    }

    PORT_Assert(mask == CIPHER_FAMILYID_SMIME);
    if (mask != CIPHER_FAMILYID_SMIME) {
        /* XXX set an error! */
        return SECFailure;
    }

    if (on) {
        PORT_Assert(smime_current_pref_index < smime_symmetric_count);
        if (smime_current_pref_index >= smime_symmetric_count) {
            /* XXX set an error! */
            return SECFailure;
        }

        smime_newprefs[smime_current_pref_index++] = which;
    }

    return SECSuccess;
}

/*
 * this function locally records the export policy
 */
SECStatus
SECMIME_SetPolicy(long which, int on)
{
    unsigned long mask;

    PORT_Assert((which & CIPHER_FAMILYID_MASK) == CIPHER_FAMILYID_SMIME);
    if ((which & CIPHER_FAMILYID_MASK) != CIPHER_FAMILYID_SMIME) {
        /* XXX set an error! */
        return SECFailure;
    }

    which &= ~CIPHER_FAMILYID_MASK;

    PORT_Assert(which < 32); /* bits in the long */
    if (which >= 32) {
        /* XXX set an error! */
        return SECFailure;
    }

    mask = 1UL << which;

    if (on) {
        smime_policy_bits |= mask;
    } else {
        smime_policy_bits &= ~mask;
    }

    return SECSuccess;
}

/*
 * Based on the given algorithm (including its parameters, in some cases!)
 * and the given key (may or may not be inspected, depending on the
 * algorithm), find the appropriate policy algorithm specification
 * and return it.  If no match can be made, -1 is returned.
 */
static long
smime_policy_algorithm(SECAlgorithmID *algid, PK11SymKey *key)
{
    SECOidTag algtag;

    algtag = SECOID_GetAlgorithmTag(algid);
    switch (algtag) {
        case SEC_OID_RC2_CBC: {
            unsigned int keylen_bits;

            keylen_bits = PK11_GetKeyStrength(key, algid);
            switch (keylen_bits) {
                case 40:
                    return SMIME_RC2_CBC_40;
                case 64:
                    return SMIME_RC2_CBC_64;
                case 128:
                    return SMIME_RC2_CBC_128;
                default:
                    break;
            }
        } break;
        case SEC_OID_DES_CBC:
            return SMIME_DES_CBC_56;
        case SEC_OID_DES_EDE3_CBC:
            return SMIME_DES_EDE3_168;
#ifdef SMIME_DOES_RC5
        case SEC_OID_RC5_CBC_PAD:
            PORT_Assert(0); /* XXX need to pull out parameters and match */
            break;
#endif
        default:
            break;
    }

    return -1;
}

static PRBool
smime_cipher_allowed(unsigned long which)
{
    unsigned long mask;

    which &= ~CIPHER_FAMILYID_MASK;
    PORT_Assert(which < 32); /* bits per long (min) */
    if (which >= 32)
        return PR_FALSE;

    mask = 1UL << which;
    if ((mask & smime_policy_bits) == 0)
        return PR_FALSE;

    return PR_TRUE;
}

PRBool
SECMIME_DecryptionAllowed(SECAlgorithmID *algid, PK11SymKey *key)
{
    long which;

    which = smime_policy_algorithm(algid, key);
    if (which < 0)
        return PR_FALSE;

    return smime_cipher_allowed((unsigned long)which);
}

/*
 * Does the current policy allow *any* S/MIME encryption (or decryption)?
 *
 * This tells whether or not *any* S/MIME encryption can be done,
 * according to policy.  Callers may use this to do nicer user interface
 * (say, greying out a checkbox so a user does not even try to encrypt
 * a message when they are not allowed to) or for any reason they want
 * to check whether S/MIME encryption (or decryption, for that matter)
 * may be done.
 *
 * It takes no arguments.  The return value is a simple boolean:
 *   PR_TRUE means encryption (or decryption) is *possible*
 *      (but may still fail due to other reasons, like because we cannot
 *      find all the necessary certs, etc.; PR_TRUE is *not* a guarantee)
 *   PR_FALSE means encryption (or decryption) is not permitted
 *
 * There are no errors from this routine.
 */
PRBool
SECMIME_EncryptionPossible(void)
{
    if (smime_policy_bits != 0)
        return PR_TRUE;

    return PR_FALSE;
}

/*
 * XXX Would like the "parameters" field to be a SECItem *, but the
 * encoder is having trouble with optional pointers to an ANY.  Maybe
 * once that is fixed, can change this back...
 */
typedef struct smime_capability_struct {
    unsigned long cipher; /* local; not part of encoding */
    SECOidTag capIDTag;   /* local; not part of encoding */
    SECItem capabilityID;
    SECItem parameters;
} smime_capability;

static const SEC_ASN1Template smime_capability_template[] = {
    { SEC_ASN1_SEQUENCE,
      0, NULL, sizeof(smime_capability) },
    { SEC_ASN1_OBJECT_ID,
      offsetof(smime_capability, capabilityID) },
    { SEC_ASN1_OPTIONAL | SEC_ASN1_ANY,
      offsetof(smime_capability, parameters) },
    { 0 }
};

static const SEC_ASN1Template smime_capabilities_template[] = {
    { SEC_ASN1_SEQUENCE_OF, 0, smime_capability_template }
};

static void
smime_fill_capability(smime_capability *cap)
{
    unsigned long cipher;
    SECOidTag algtag;
    int i;

    algtag = SECOID_FindOIDTag(&(cap->capabilityID));

    for (i = 0; i < smime_symmetric_count; i++) {
        if (smime_cipher_maps[i].algtag != algtag)
            continue;
        /*
         * XXX If SECITEM_CompareItem allowed NULLs as arguments (comparing
         * 2 NULLs as equal and NULL and non-NULL as not equal), we could
         * use that here instead of all of the following comparison code.
         */
        if (cap->parameters.data != NULL) {
            if (smime_cipher_maps[i].parms == NULL)
                continue;
            if (cap->parameters.len != smime_cipher_maps[i].parms->len)
                continue;
            if (PORT_Memcmp(cap->parameters.data,
                            smime_cipher_maps[i].parms->data,
                            cap->parameters.len) == 0)
                break;
        } else if (smime_cipher_maps[i].parms == NULL) {
            break;
        }
    }

    if (i == smime_symmetric_count)
        cipher = 0;
    else
        cipher = smime_cipher_maps[i].cipher;

    cap->cipher = cipher;
    cap->capIDTag = algtag;
}

static long
smime_choose_cipher(CERTCertificate *scert, CERTCertificate **rcerts)
{
    PLArenaPool *poolp;
    long chosen_cipher;
    int *cipher_abilities;
    int *cipher_votes;
    int strong_mapi;
    int rcount, mapi, max;

    if (smime_policy_bits == 0) {
        PORT_SetError(SEC_ERROR_BAD_EXPORT_ALGORITHM);
        return -1;
    }

    chosen_cipher = SMIME_RC2_CBC_40; /* the default, LCD */

    poolp = PORT_NewArena(1024); /* XXX what is right value? */
    if (poolp == NULL)
        goto done;

    cipher_abilities = (int *)PORT_ArenaZAlloc(poolp,
                                               smime_symmetric_count * sizeof(int));
    if (cipher_abilities == NULL)
        goto done;

    cipher_votes = (int *)PORT_ArenaZAlloc(poolp,
                                           smime_symmetric_count * sizeof(int));
    if (cipher_votes == NULL)
        goto done;

    /*
     * XXX Should have a #define somewhere which specifies default
     * strong cipher.  (Or better, a way to configure.)
     */

    /* Make triple-DES the strong cipher. */
    strong_mapi = smime_mapi_by_cipher(SMIME_DES_EDE3_168);

    PORT_Assert(strong_mapi >= 0);

    for (rcount = 0; rcerts[rcount] != NULL; rcount++) {
        SECItem *profile;
        smime_capability **caps;
        int capi, pref;
        SECStatus dstat;

        pref = smime_symmetric_count;
        profile = CERT_FindSMimeProfile(rcerts[rcount]);
        if (profile != NULL && profile->data != NULL && profile->len > 0) {
            caps = NULL;
            dstat = SEC_QuickDERDecodeItem(poolp, &caps,
                                           smime_capabilities_template,
                                           profile);
            if (dstat == SECSuccess && caps != NULL) {
                for (capi = 0; caps[capi] != NULL; capi++) {
                    smime_fill_capability(caps[capi]);
                    mapi = smime_mapi_by_cipher(caps[capi]->cipher);
                    if (mapi >= 0) {
                        cipher_abilities[mapi]++;
                        cipher_votes[mapi] += pref;
                        --pref;
                    }
                }
            }
        } else {
            SECKEYPublicKey *key;
            unsigned int pklen_bits;

            /*
             * XXX This is probably only good for RSA keys.  What I would
             * really like is a function to just say;  Is the public key in
             * this cert an export-length key?  Then I would not have to
             * know things like the value 512, or the kind of key, or what
             * a subjectPublicKeyInfo is, etc.
             */
            key = CERT_ExtractPublicKey(rcerts[rcount]);
            if (key != NULL) {
                pklen_bits = SECKEY_PublicKeyStrength(key) * 8;
                SECKEY_DestroyPublicKey(key);

                if (pklen_bits > 512) {
                    cipher_abilities[strong_mapi]++;
                    cipher_votes[strong_mapi] += pref;
                }
            }
        }
        if (profile != NULL)
            SECITEM_FreeItem(profile, PR_TRUE);
    }

    max = 0;
    for (mapi = 0; mapi < smime_symmetric_count; mapi++) {
        if (cipher_abilities[mapi] != rcount)
            continue;
        if (!smime_cipher_allowed(smime_cipher_maps[mapi].cipher))
            continue;
        if (cipher_votes[mapi] > max) {
            chosen_cipher = smime_cipher_maps[mapi].cipher;
            max = cipher_votes[mapi];
        } /* XXX else if a tie, let scert break it? */
    }

done:
    if (poolp != NULL)
        PORT_FreeArena(poolp, PR_FALSE);

    return chosen_cipher;
}

/*
 * XXX This is a hack for now to satisfy our current interface.
 * Eventually, with more parameters needing to be specified, just
 * looking up the keysize is not going to be sufficient.
 */
static int
smime_keysize_by_cipher(unsigned long which)
{
    int keysize;

    switch (which) {
        case SMIME_RC2_CBC_40:
            keysize = 40;
            break;
        case SMIME_RC2_CBC_64:
            keysize = 64;
            break;
        case SMIME_RC2_CBC_128:
            keysize = 128;
            break;
#ifdef SMIME_DOES_RC5
        case SMIME_RC5PAD_64_16_40:
        case SMIME_RC5PAD_64_16_64:
        case SMIME_RC5PAD_64_16_128:
            /* XXX See comment above; keysize is not enough... */
            PORT_Assert(0);
            PORT_SetError(SEC_ERROR_INVALID_ALGORITHM);
            keysize = -1;
            break;
#endif
        case SMIME_DES_CBC_56:
        case SMIME_DES_EDE3_168:
            /*
             * These are special; since the key size is fixed, we actually
             * want to *avoid* specifying a key size.
             */
            keysize = 0;
            break;
        default:
            keysize = -1;
            break;
    }

    return keysize;
}

/*
 * Start an S/MIME encrypting context.
 *
 * "scert" is the cert for the sender.  It will be checked for validity.
 * "rcerts" are the certs for the recipients.  They will also be checked.
 *
 * "certdb" is the cert database to use for verifying the certs.
 * It can be NULL if a default database is available (like in the client).
 *
 * This function already does all of the stuff specific to S/MIME protocol
 * and local policy; the return value just needs to be passed to
 * SEC_PKCS7Encode() or to SEC_PKCS7EncoderStart() to create the encoded data,
 * and finally to SEC_PKCS7DestroyContentInfo().
 *
 * An error results in a return value of NULL and an error set.
 * (Retrieve specific errors via PORT_GetError()/XP_GetError().)
 */
SEC_PKCS7ContentInfo *
SECMIME_CreateEncrypted(CERTCertificate *scert,
                        CERTCertificate **rcerts,
                        CERTCertDBHandle *certdb,
                        SECKEYGetPasswordKey pwfn,
                        void *pwfn_arg)
{
    SEC_PKCS7ContentInfo *cinfo;
    long cipher;
    SECOidTag encalg;
    int keysize;
    int mapi, rci;

    cipher = smime_choose_cipher(scert, rcerts);
    if (cipher < 0)
        return NULL;

    mapi = smime_mapi_by_cipher(cipher);
    if (mapi < 0)
        return NULL;

    /*
     * XXX This is stretching it -- CreateEnvelopedData should probably
     * take a cipher itself of some sort, because we cannot know what the
     * future will bring in terms of parameters for each type of algorithm.
     * For example, just an algorithm and keysize is *not* sufficient to
     * fully specify the usage of RC5 (which also needs to know rounds and
     * block size).  Work this out into a better API!
     */
    encalg = smime_cipher_maps[mapi].algtag;
    keysize = smime_keysize_by_cipher(cipher);
    if (keysize < 0)
        return NULL;

    cinfo = SEC_PKCS7CreateEnvelopedData(scert, certUsageEmailRecipient,
                                         certdb, encalg, keysize,
                                         pwfn, pwfn_arg);
    if (cinfo == NULL)
        return NULL;

    for (rci = 0; rcerts[rci] != NULL; rci++) {
        if (rcerts[rci] == scert)
            continue;
        if (SEC_PKCS7AddRecipient(cinfo, rcerts[rci], certUsageEmailRecipient,
                                  NULL) != SECSuccess) {
            SEC_PKCS7DestroyContentInfo(cinfo);
            return NULL;
        }
    }

    return cinfo;
}

static smime_capability **smime_capabilities;
static SECItem *smime_encoded_caps;

static SECStatus
smime_init_caps(void)
{
    smime_capability *cap;
    smime_cipher_map *map;
    SECOidData *oiddata;
    SECStatus rv;
    int i;

    if (smime_encoded_caps != NULL && (!smime_prefs_changed))
        return SECSuccess;

    if (smime_encoded_caps != NULL) {
        SECITEM_FreeItem(smime_encoded_caps, PR_TRUE);
        smime_encoded_caps = NULL;
    }

    if (smime_capabilities == NULL) {
        smime_capabilities = (smime_capability **)PORT_ZAlloc(
            (smime_symmetric_count + 1) * sizeof(smime_capability *));
        if (smime_capabilities == NULL)
            return SECFailure;
    }

    rv = SECFailure;

    /*
       The process of creating the encoded PKCS7 cipher capability list
       involves two basic steps:

       (a) Convert our internal representation of cipher preferences
           (smime_prefs) into an array containing cipher OIDs and
           parameter data (smime_capabilities). This step is
           performed here.

       (b) Encode, using ASN.1, the cipher information in
           smime_capabilities, leaving the encoded result in
           smime_encoded_caps.

       (In the process of performing (a), Lisa put in some optimizations
       which allow us to avoid needlessly re-populating elements in
       smime_capabilities as we walk through smime_prefs.)
    */
    for (i = 0; i < smime_current_pref_index; i++) {
        int mapi;

        /* Get the next cipher preference in smime_prefs. */
        mapi = smime_mapi_by_cipher(smime_prefs[i]);
        if (mapi < 0)
            break;

        /* Find the corresponding entry in the cipher map. */
        PORT_Assert(mapi < smime_symmetric_count);
        map = &(smime_cipher_maps[mapi]);

        /*
         * Convert the next preference found in smime_prefs into an
         * smime_capability.
         */

        cap = smime_capabilities[i];
        if (cap == NULL) {
            cap = (smime_capability *)PORT_ZAlloc(sizeof(smime_capability));
            if (cap == NULL)
                break;
            smime_capabilities[i] = cap;
        } else if (cap->cipher == smime_prefs[i]) {
            continue; /* no change to this one */
        }

        cap->capIDTag = map->algtag;
        oiddata = SECOID_FindOIDByTag(map->algtag);
        if (oiddata == NULL)
            break;

        if (cap->capabilityID.data != NULL) {
            SECITEM_FreeItem(&(cap->capabilityID), PR_FALSE);
            cap->capabilityID.data = NULL;
            cap->capabilityID.len = 0;
        }

        rv = SECITEM_CopyItem(NULL, &(cap->capabilityID), &(oiddata->oid));
        if (rv != SECSuccess)
            break;

        if (map->parms == NULL) {
            cap->parameters.data = NULL;
            cap->parameters.len = 0;
        } else {
            cap->parameters.data = map->parms->data;
            cap->parameters.len = map->parms->len;
        }

        cap->cipher = smime_prefs[i];
    }

    if (i != smime_current_pref_index)
        return rv;

    while (i < smime_symmetric_count) {
        cap = smime_capabilities[i];
        if (cap != NULL) {
            SECITEM_FreeItem(&(cap->capabilityID), PR_FALSE);
            PORT_Free(cap);
        }
        smime_capabilities[i] = NULL;
        i++;
    }
    smime_capabilities[i] = NULL;

    smime_encoded_caps = SEC_ASN1EncodeItem(NULL, NULL, &smime_capabilities,
                                            smime_capabilities_template);
    if (smime_encoded_caps == NULL)
        return SECFailure;

    return SECSuccess;
}

static SECStatus
smime_add_profile(CERTCertificate *cert, SEC_PKCS7ContentInfo *cinfo)
{
    PORT_Assert(smime_prefs_complete);
    if (!smime_prefs_complete)
        return SECFailure;

    /* For that matter, if capabilities haven't been initialized yet,
       do so now. */
    if (smime_encoded_caps == NULL || smime_prefs_changed) {
        SECStatus rv;

        rv = smime_init_caps();
        if (rv != SECSuccess)
            return rv;

        PORT_Assert(smime_encoded_caps != NULL);
    }

    return SEC_PKCS7AddSignedAttribute(cinfo, SEC_OID_PKCS9_SMIME_CAPABILITIES,
                                       smime_encoded_caps);
}

/*
 * Start an S/MIME signing context.
 *
 * "scert" is the cert that will be used to sign the data.  It will be
 * checked for validity.
 *
 * "ecert" is the signer's encryption cert.  If it is different from
 * scert, then it will be included in the signed message so that the
 * recipient can save it for future encryptions.
 *
 * "certdb" is the cert database to use for verifying the cert.
 * It can be NULL if a default database is available (like in the client).
 *
 * "digestalg" names the digest algorithm (e.g. SEC_OID_SHA1).
 * XXX There should be SECMIME functions for hashing, or the hashing should
 * be built into this interface, which we would like because we would
 * support more smartcards that way, and then this argument should go away.)
 *
 * "digest" is the actual digest of the data.  It must be provided in
 * the case of detached data or NULL if the content will be included.
 *
 * This function already does all of the stuff specific to S/MIME protocol
 * and local policy; the return value just needs to be passed to
 * SEC_PKCS7Encode() or to SEC_PKCS7EncoderStart() to create the encoded data,
 * and finally to SEC_PKCS7DestroyContentInfo().
 *
 * An error results in a return value of NULL and an error set.
 * (Retrieve specific errors via PORT_GetError()/XP_GetError().)
 */

SEC_PKCS7ContentInfo *
SECMIME_CreateSigned(CERTCertificate *scert,
                     CERTCertificate *ecert,
                     CERTCertDBHandle *certdb,
                     SECOidTag digestalg,
                     SECItem *digest,
                     SECKEYGetPasswordKey pwfn,
                     void *pwfn_arg)
{
    SEC_PKCS7ContentInfo *cinfo;
    SECStatus rv;

    /* See note in header comment above about digestalg. */
    /* Doesn't explain this. PORT_Assert (digestalg == SEC_OID_SHA1); */

    cinfo = SEC_PKCS7CreateSignedData(scert, certUsageEmailSigner,
                                      certdb, digestalg, digest,
                                      pwfn, pwfn_arg);
    if (cinfo == NULL)
        return NULL;

    if (SEC_PKCS7IncludeCertChain(cinfo, NULL) != SECSuccess) {
        SEC_PKCS7DestroyContentInfo(cinfo);
        return NULL;
    }

    /* if the encryption cert and the signing cert differ, then include
     * the encryption cert too.
     */
    /* it is ok to compare the pointers since we ref count, and the same
     * cert will always have the same pointer
     */
    if ((ecert != NULL) && (ecert != scert)) {
        rv = SEC_PKCS7AddCertificate(cinfo, ecert);
        if (rv != SECSuccess) {
            SEC_PKCS7DestroyContentInfo(cinfo);
            return NULL;
        }
    }
    /*
     * Add the signing time.  But if it fails for some reason,
     * may as well not give up altogether -- just assert.
     */
    rv = SEC_PKCS7AddSigningTime(cinfo);
    PORT_Assert(rv == SECSuccess);

    /*
     * Add the email profile.  Again, if it fails for some reason,
     * may as well not give up altogether -- just assert.
     */
    rv = smime_add_profile(ecert, cinfo);
    PORT_Assert(rv == SECSuccess);

    return cinfo;
}
