bug 686149 - improve PKCS7 certificate export to not use legacy path building r=fkiefer
authorDavid Keeler <dkeeler@mozilla.com>
Wed, 02 May 2018 10:22:58 -0700
changeset 417486 3c65a2197eadf11fd6f3a87caf7682cc89558cbd
parent 417485 3029b0beea0eb25cb9b81de89aed56f1f95a0b89
child 417487 35e7f3b5f06cf96a3738eaeba1cffa02eea36fc4
push id33968
push userebalazs@mozilla.com
push dateWed, 09 May 2018 09:32:53 +0000
treeherdermozilla-central@a2eccfbeb0ae [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersfkiefer
bugs686149
milestone62.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
bug 686149 - improve PKCS7 certificate export to not use legacy path building r=fkiefer MozReview-Commit-ID: 2U4J8uUlvaN
security/manager/pki/resources/content/pippki.js
security/manager/ssl/nsIX509Cert.idl
security/manager/ssl/nsIX509CertList.idl
security/manager/ssl/nsNSSCertificate.cpp
security/manager/ssl/tests/unit/head_psm.js
security/manager/ssl/tests/unit/test_cert_chains.js
--- a/security/manager/pki/resources/content/pippki.js
+++ b/security/manager/pki/resources/content/pippki.js
@@ -40,24 +40,23 @@ function getDERString(cert) {
   var derArray = cert.getRawDER(length);
   var derString = "";
   for (var i = 0; i < derArray.length; i++) {
     derString += String.fromCharCode(derArray[i]);
   }
   return derString;
 }
 
-function getPKCS7String(cert, chainMode) {
-  var length = {};
-  var pkcs7Array = cert.exportAsCMS(chainMode, length);
-  var pkcs7String = "";
-  for (var i = 0; i < pkcs7Array.length; i++) {
-    pkcs7String += String.fromCharCode(pkcs7Array[i]);
+function getPKCS7String(certArray) {
+  let certList = Cc["@mozilla.org/security/x509certlist;1"]
+                   .createInstance(Ci.nsIX509CertList);
+  for (let cert of certArray) {
+    certList.addCert(cert);
   }
-  return pkcs7String;
+  return certList.asPKCS7Blob();
 }
 
 function getPEMString(cert) {
   var derb64 = btoa(getDERString(cert));
   // Wrap the Base64 string into lines of 64 characters with CRLF line breaks
   // (as specified in RFC 1421).
   var wrapped = derb64.replace(/(\S{64}(?!$))/g, "$1\r\n");
   return "-----BEGIN CERTIFICATE-----\r\n"
@@ -142,20 +141,20 @@ async function exportToFile(parent, cert
         for (let i = 1; i < chain.length; i++) {
           content += getPEMString(chain[i]);
         }
         break;
       case 2:
         content = getDERString(cert);
         break;
       case 3:
-        content = getPKCS7String(cert, Ci.nsIX509Cert.CMS_CHAIN_MODE_CertOnly);
+        content = getPKCS7String([cert]);
         break;
       case 4:
-        content = getPKCS7String(cert, Ci.nsIX509Cert.CMS_CHAIN_MODE_CertChainWithRoot);
+        content = getPKCS7String(chain);
         break;
       case 0:
       default:
         content = getPEMString(cert);
         break;
     }
     var msg;
     var written = 0;
--- a/security/manager/ssl/nsIX509Cert.idl
+++ b/security/manager/ssl/nsIX509Cert.idl
@@ -171,23 +171,16 @@ interface nsIX509Cert : nsISupports {
   /**
    *  True if the certificate is self-signed. CA issued
    *  certificates are always self-signed.
    */
   [must_use]
   readonly attribute boolean isSelfSigned;
 
   /**
-   *  Constants for specifying the chain mode when exporting a certificate
-   */
-  const unsigned long CMS_CHAIN_MODE_CertOnly = 1;
-  const unsigned long CMS_CHAIN_MODE_CertChain = 2;
-  const unsigned long CMS_CHAIN_MODE_CertChainWithRoot = 3;
-
-  /**
    * A comma separated list of localized strings representing the contents of
    * the certificate's key usage extension, if present. The empty string if the
    * certificate doesn't have the key usage extension, or has an empty extension.
    */
   [must_use]
   readonly attribute AString keyUsages;
 
   /**
@@ -221,30 +214,16 @@ interface nsIX509Cert : nsISupports {
   /**
    * The base64 encoding of the DER encoded public key info using the specified
    * digest.
    */
   [must_use]
   readonly attribute ACString sha256SubjectPublicKeyInfoDigest;
 
   /**
-   *  Obtain the certificate wrapped in a PKCS#7 SignedData structure,
-   *  with or without the certificate chain
-   *
-   *  @param chainMode Whether to include the chain (with or without the root),
-                       see CMS_CHAIN_MODE constants.
-   *  @param length The number of bytes of the PKCS#7 data.
-   *  @param data The bytes representing the PKCS#7 wrapped certificate.
-   */
-  [must_use]
-  void exportAsCMS(in unsigned long chainMode,
-                   out unsigned long length,
-                   [retval, array, size_is(length)] out octet data);
-
-  /**
    * Retrieves the NSS certificate object wrapped by this interface
    */
   [notxpcom, noscript, must_use]
   CERTCertificatePtr getCert();
 
   /**
    * Either delete the certificate from all cert databases,
    * or mark it as untrusted.
--- a/security/manager/ssl/nsIX509CertList.idl
+++ b/security/manager/ssl/nsIX509CertList.idl
@@ -39,16 +39,25 @@ interface nsIX509CertList : nsISupports 
   [must_use]
   boolean equals(in nsIX509CertList other);
 
   /**
    * Retrieves the PSM helper class that wraps the NSS certificate list
    */
   [notxpcom, noscript, must_use]
   nsNSSCertListPtr getCertList();
+
+  /**
+   * Encode the list of certificates as a PKCS#7 SignedData structure. No data
+   * is actually signed - this is merely a way of exporting a collection of
+   * certificates.
+   */
+  [must_use]
+  ACString asPKCS7Blob();
+
 };
 
 %{C++
 
 #define NS_X509CERTLIST_CID { /* 959fb165-6517-487f-ab9b-d8913be53197 */  \
     0x959fb165,                                                           \
     0x6517,                                                               \
     0x487f,                                                               \
--- a/security/manager/ssl/nsNSSCertificate.cpp
+++ b/security/manager/ssl/nsNSSCertificate.cpp
@@ -750,142 +750,16 @@ nsNSSCertificate::GetRawDER(uint32_t* aL
       *aLength = mCert->derCert.len;
       return NS_OK;
     }
   }
   *aLength = 0;
   return NS_ERROR_FAILURE;
 }
 
-NS_IMETHODIMP
-nsNSSCertificate::ExportAsCMS(uint32_t chainMode,
-                              uint32_t* aLength, uint8_t** aArray)
-{
-  NS_ENSURE_ARG(aLength);
-  NS_ENSURE_ARG(aArray);
-
-  nsresult rv = BlockUntilLoadableRootsLoaded();
-  if (NS_FAILED(rv)) {
-    return rv;
-  }
-
-  if (!mCert)
-    return NS_ERROR_FAILURE;
-
-  switch (chainMode) {
-    case nsIX509Cert::CMS_CHAIN_MODE_CertOnly:
-    case nsIX509Cert::CMS_CHAIN_MODE_CertChain:
-    case nsIX509Cert::CMS_CHAIN_MODE_CertChainWithRoot:
-      break;
-    default:
-      return NS_ERROR_INVALID_ARG;
-  }
-
-  UniqueNSSCMSMessage cmsg(NSS_CMSMessage_Create(nullptr));
-  if (!cmsg) {
-    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-           ("nsNSSCertificate::ExportAsCMS - can't create CMS message\n"));
-    return NS_ERROR_OUT_OF_MEMORY;
-  }
-
-  // first, create SignedData with the certificate only (no chain)
-  UniqueNSSCMSSignedData sigd(
-    NSS_CMSSignedData_CreateCertsOnly(cmsg.get(), mCert.get(), false));
-  if (!sigd) {
-    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-           ("nsNSSCertificate::ExportAsCMS - can't create SignedData\n"));
-    return NS_ERROR_FAILURE;
-  }
-
-  // Calling NSS_CMSSignedData_CreateCertsOnly() will not allow us
-  // to specify the inclusion of the root, but CERT_CertChainFromCert() does.
-  // Since CERT_CertChainFromCert() also includes the certificate itself,
-  // we have to start at the issuing cert (to avoid duplicate certs
-  // in the SignedData).
-  if (chainMode == nsIX509Cert::CMS_CHAIN_MODE_CertChain ||
-      chainMode == nsIX509Cert::CMS_CHAIN_MODE_CertChainWithRoot) {
-    UniqueCERTCertificate issuerCert(
-      CERT_FindCertIssuer(mCert.get(), PR_Now(), certUsageAnyCA));
-    // the issuerCert of a self signed root is the cert itself,
-    // so make sure we're not adding duplicates, again
-    if (issuerCert && issuerCert != mCert) {
-      bool includeRoot =
-        (chainMode == nsIX509Cert::CMS_CHAIN_MODE_CertChainWithRoot);
-      UniqueCERTCertificateList certChain(
-        CERT_CertChainFromCert(issuerCert.get(), certUsageAnyCA, includeRoot));
-      if (certChain) {
-        if (NSS_CMSSignedData_AddCertList(sigd.get(), certChain.get())
-              == SECSuccess) {
-          Unused << certChain.release();
-        }
-        else {
-          MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-                 ("nsNSSCertificate::ExportAsCMS - can't add chain\n"));
-          return NS_ERROR_FAILURE;
-        }
-      }
-      else {
-        // try to add the issuerCert, at least
-        if (NSS_CMSSignedData_AddCertificate(sigd.get(), issuerCert.get())
-              == SECSuccess) {
-          Unused << issuerCert.release();
-        }
-        else {
-          MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-                 ("nsNSSCertificate::ExportAsCMS - can't add issuer cert\n"));
-          return NS_ERROR_FAILURE;
-        }
-      }
-    }
-  }
-
-  NSSCMSContentInfo* cinfo = NSS_CMSMessage_GetContentInfo(cmsg.get());
-  if (NSS_CMSContentInfo_SetContent_SignedData(cmsg.get(), cinfo, sigd.get())
-       == SECSuccess) {
-    Unused << sigd.release();
-  }
-  else {
-    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-           ("nsNSSCertificate::ExportAsCMS - can't attach SignedData\n"));
-    return NS_ERROR_FAILURE;
-  }
-
-  UniquePLArenaPool arena(PORT_NewArena(1024));
-  if (!arena) {
-    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-           ("nsNSSCertificate::ExportAsCMS - out of memory\n"));
-    return NS_ERROR_OUT_OF_MEMORY;
-  }
-
-  SECItem certP7 = { siBuffer, nullptr, 0 };
-  NSSCMSEncoderContext* ecx = NSS_CMSEncoder_Start(cmsg.get(), nullptr, nullptr,
-                                                   &certP7, arena.get(), nullptr,
-                                                   nullptr, nullptr, nullptr,
-                                                   nullptr, nullptr);
-  if (!ecx) {
-    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-           ("nsNSSCertificate::ExportAsCMS - can't create encoder context\n"));
-    return NS_ERROR_FAILURE;
-  }
-
-  if (NSS_CMSEncoder_Finish(ecx) != SECSuccess) {
-    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
-           ("nsNSSCertificate::ExportAsCMS - failed to add encoded data\n"));
-    return NS_ERROR_FAILURE;
-  }
-
-  *aArray = (uint8_t*)moz_xmalloc(certP7.len);
-  if (!*aArray)
-    return NS_ERROR_OUT_OF_MEMORY;
-
-  memcpy(*aArray, certP7.data, certP7.len);
-  *aLength = certP7.len;
-  return NS_OK;
-}
-
 CERTCertificate*
 nsNSSCertificate::GetCert()
 {
   return (mCert) ? CERT_DupCertificate(mCert.get()) : nullptr;
 }
 
 NS_IMETHODIMP
 nsNSSCertificate::GetValidity(nsIX509CertValidity** aValidity)
@@ -1042,16 +916,96 @@ nsNSSCertList::DupCertList(const UniqueC
 
 CERTCertList*
 nsNSSCertList::GetRawCertList()
 {
   return mCertList.get();
 }
 
 NS_IMETHODIMP
+nsNSSCertList::AsPKCS7Blob(/*out*/ nsACString& result)
+{
+  MOZ_ASSERT(mCertList);
+  if (!mCertList) {
+    return NS_ERROR_FAILURE;
+  }
+
+  UniqueNSSCMSMessage cmsg(NSS_CMSMessage_Create(nullptr));
+  if (!cmsg) {
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+            ("nsNSSCertList::AsPKCS7Blob - can't create CMS message"));
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+
+  UniqueNSSCMSSignedData sigd(nullptr);
+  nsresult rv = ForEachCertificateInChain(
+    [&cmsg, &sigd] (nsCOMPtr<nsIX509Cert> aCert, bool /*unused*/,
+            /*out*/ bool& /*unused*/) {
+      // We need an owning handle when calling nsIX509Cert::GetCert().
+      UniqueCERTCertificate nssCert(aCert->GetCert());
+      if (!sigd) {
+        sigd.reset(NSS_CMSSignedData_CreateCertsOnly(cmsg.get(), nssCert.get(),
+                                                     false));
+        if (!sigd) {
+          MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+                  ("nsNSSCertList::AsPKCS7Blob - can't create SignedData"));
+          return NS_ERROR_FAILURE;
+        }
+      } else if (NSS_CMSSignedData_AddCertificate(sigd.get(), nssCert.get())
+                   != SECSuccess) {
+        MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+                ("nsNSSCertList::AsPKCS7Blob - can't add cert"));
+        return NS_ERROR_FAILURE;
+      }
+      return NS_OK;
+  });
+  if (NS_FAILED(rv)) {
+    return rv;
+  }
+
+  NSSCMSContentInfo* cinfo = NSS_CMSMessage_GetContentInfo(cmsg.get());
+  if (NSS_CMSContentInfo_SetContent_SignedData(cmsg.get(), cinfo, sigd.get())
+        != SECSuccess) {
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+            ("nsNSSCertList::AsPKCS7Blob - can't attach SignedData"));
+    return NS_ERROR_FAILURE;
+  }
+  // cmsg owns sigd now.
+  Unused << sigd.release();
+
+  UniquePLArenaPool arena(PORT_NewArena(1024));
+  if (!arena) {
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+            ("nsNSSCertList::AsPKCS7Blob - out of memory"));
+    return NS_ERROR_OUT_OF_MEMORY;
+  }
+
+  SECItem certP7 = { siBuffer, nullptr, 0 };
+  NSSCMSEncoderContext* ecx = NSS_CMSEncoder_Start(cmsg.get(), nullptr, nullptr,
+                                                   &certP7, arena.get(), nullptr,
+                                                   nullptr, nullptr, nullptr,
+                                                   nullptr, nullptr);
+  if (!ecx) {
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+            ("nsNSSCertList::AsPKCS7Blob - can't create encoder"));
+    return NS_ERROR_FAILURE;
+  }
+
+  if (NSS_CMSEncoder_Finish(ecx) != SECSuccess) {
+    MOZ_LOG(gPIPNSSLog, LogLevel::Debug,
+            ("nsNSSCertList::AsPKCS7Blob - failed to add encoded data"));
+    return NS_ERROR_FAILURE;
+  }
+
+  result.Assign(nsDependentCSubstring(reinterpret_cast<const char*>(certP7.data),
+                                      certP7.len));
+  return NS_OK;
+}
+
+NS_IMETHODIMP
 nsNSSCertList::Write(nsIObjectOutputStream* aStream)
 {
   NS_ENSURE_STATE(mCertList);
   nsresult rv = NS_OK;
 
   // First, enumerate the certs to get the length of the list
   uint32_t certListLen = 0;
   CERTCertListNode* node = nullptr;
--- a/security/manager/ssl/tests/unit/head_psm.js
+++ b/security/manager/ssl/tests/unit/head_psm.js
@@ -118,21 +118,21 @@ const NO_FLAGS = 0;
 // with no newlines or BEGIN/END headers. This is a helper function to convert
 // PEM to the format that nsIX509CertDB requires.
 function pemToBase64(pem) {
   return pem.replace(/-----BEGIN CERTIFICATE-----/, "")
             .replace(/-----END CERTIFICATE-----/, "")
             .replace(/[\r\n]/g, "");
 }
 
-function build_cert_chain(certNames) {
+function build_cert_chain(certNames, testDirectory = "bad_certs") {
   let certList = Cc["@mozilla.org/security/x509certlist;1"]
                    .createInstance(Ci.nsIX509CertList);
   certNames.forEach(function(certName) {
-    let cert = constructCertFromFile("bad_certs/" + certName + ".pem");
+    let cert = constructCertFromFile(`${testDirectory}/${certName}.pem`);
     certList.addCert(cert);
   });
   return certList;
 }
 
 function readFile(file) {
   let fstream = Cc["@mozilla.org/network/file-input-stream;1"]
                   .createInstance(Ci.nsIFileInputStream);
--- a/security/manager/ssl/tests/unit/test_cert_chains.js
+++ b/security/manager/ssl/tests/unit/test_cert_chains.js
@@ -32,16 +32,150 @@ function test_cert_list_serialization() 
 
   // Deserialize from the string and compare to the original object
   let deserialized = serHelper.deserializeObject(serialized);
   deserialized.QueryInterface(Ci.nsIX509CertList);
   ok(certList.equals(deserialized),
      "Deserialized cert list should equal the original");
 }
 
+function test_cert_pkcs7_export() {
+  // This was generated by running BadCertServer locally on the bad_certs
+  // directory and visiting:
+  // https://good.include-subdomains.pinning.example.com:8443/
+  // and then viewing the certificate chain presented (in the page info dialog)
+  // and exporting it.
+  // (NB: test-ca must be imported and trusted for the connection to succeed)
+  const expectedPKCS7ForDefaultEE =
+    "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";
+  let certListDefaultEE = build_cert_chain(["default-ee", "test-ca"]);
+  let pkcs7DefaultEE = certListDefaultEE.asPKCS7Blob();
+  equal(btoa(pkcs7DefaultEE), expectedPKCS7ForDefaultEE,
+        "PKCS7 export should work as expected for default-ee chain");
+
+  // This was generated by running BadCertServer locally on the bad_certs
+  // directory and visiting:
+  // https://unknownissuer.example.com:8443/
+  // and then viewing the certificate presented (in the add certificate
+  // exception dialog) and exporting it.
+  const expectedPKCS7ForUnknownIssuer =
+    "MIAGCSqGSIb3DQEHAqCAMIACAQExADCABgkqhkiG9w0BBwEAAKCCA60wggOpMIICkaADAgE" +
+    "CAhQN8MG4SddxqOKNAeWywCf4fPyU0jANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDDBtUZX" +
+    "N0IEludGVybWVkaWF0ZSB0byBkZWxldGUwIhgPMjAxNjExMjcwMDAwMDBaGA8yMDE5MDIwN" +
+    "TAwMDAwMFowLjEsMCoGA1UEAwwjVGVzdCBFbmQtZW50aXR5IGZyb20gdW5rbm93biBpc3N1" +
+    "ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6iFGoRI4W1kH9braIBjYQPTw" +
+    "T2erkNUq07PVoV2wke8HHJajg2B+9sZwGm24ahvJr4q9adWtqZHEIeqVap0WH9xzVJJwCfs" +
+    "1D/B5p0DggKZOrIMNJ5Nu5TMJrbA7tFYIP8X6taRqx0wI6iypB7qdw4A8Njf1mCyuwJJKkf" +
+    "bmIYXmQsVeQPdI7xeC4SB+oN9OIQ+8nFthVt2Zaqn4CkC86exCABiTMHGyXrZZhW7filhLA" +
+    "dTGjDJHdtMr3/K0dJdMJ77kXDqdo4bN7LyJvaeO0ipVhHe4m1iWdq5EITjbLHCQELL8Wiy/" +
+    "l8Y+ZFzG4s/5JI/pyUcQx1QOs2hgKNe2NAgMBAAGjgcIwgb8wgYgGA1UdEQSBgDB+ghl1bm" +
+    "tub3duaXNzdWVyLmV4YW1wbGUuY29tgjR1bmtub3duaXNzdWVyLmluY2x1ZGUtc3ViZG9tY" +
+    "Wlucy5waW5uaW5nLmV4YW1wbGUuY29tgit1bmtub3duaXNzdWVyLnRlc3QtbW9kZS5waW5u" +
+    "aW5nLmV4YW1wbGUuY29tMDIGCCsGAQUFBwEBBCYwJDAiBggrBgEFBQcwAYYWaHR0cDovL2x" +
+    "vY2FsaG9zdDo4ODg4LzANBgkqhkiG9w0BAQsFAAOCAQEAW4Vm4ekBw7QmfxOzy6HsXN8SFh" +
+    "pLpE0AD1tl0fHR8V30rM8IhrnCi6KzAWceIMY4mesE+hGSkNLArNuRCI6NtGO5TyEGHhNdr" +
+    "D8CDQ+/w9by5TJIw/ZsWi2xgFRHsrmphQ8e833FEdBuhthOXwaXiVCn9/Ug6OpgsSWJp9T7" +
+    "AAiwQy2L2MseyfS9kcV/W/9kHiT4NoIz/3hVeLBiTg8silFGaEB02yLomp1T+/rkpqEhNFI" +
+    "JhZXCa1fL8u3Z/TN40nd756fPfHXwAVuyc5qkr5R1ulVbLKXCoJLzNicfSogsmzveNrhIv2" +
+    "LP6G2P9YwjmJXM6pXlxFpsWPoyi/Z4LjEAAAAAAAAA";
+  let certListUnknownIssuer = build_cert_chain(["unknownissuer"]);
+  let pkcs7UnknownIssuer = certListUnknownIssuer.asPKCS7Blob();
+  equal(btoa(pkcs7UnknownIssuer), expectedPKCS7ForUnknownIssuer,
+        "PKCS7 export should work as expected for unknown issuer");
+
+  // This was generated by running OCSPStaplingServer locally on the ocsp_certs
+  // directory and visiting:
+  // https://ocsp-stapling-with-intermediate.example.com:8443/
+  // and then viewing the certificate chain presented (in the page info dialog)
+  // and exporting it.
+  // (NB: test-ca must be imported and trusted for the connection to succeed)
+  const expectedPKCS7WithIntermediate =
+    "MIAGCSqGSIb3DQEHAqCAMIACAQExADCABgkqhkiG9w0BBwEAAKCCCPEwggLTMIIBu6ADAgE" +
+    "CAhRdBTvvC7swO3cbVWIGn/56DrQ+cjANBgkqhkiG9w0BAQsFADASMRAwDgYDVQQDDAdUZX" +
+    "N0IENBMCIYDzIwMTYxMTI3MDAwMDAwWhgPMjAxOTAyMDUwMDAwMDBaMBIxEDAOBgNVBAMMB" +
+    "1Rlc3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6iFGoRI4W1kH9braI" +
+    "BjYQPTwT2erkNUq07PVoV2wke8HHJajg2B+9sZwGm24ahvJr4q9adWtqZHEIeqVap0WH9xz" +
+    "VJJwCfs1D/B5p0DggKZOrIMNJ5Nu5TMJrbA7tFYIP8X6taRqx0wI6iypB7qdw4A8Njf1mCy" +
+    "uwJJKkfbmIYXmQsVeQPdI7xeC4SB+oN9OIQ+8nFthVt2Zaqn4CkC86exCABiTMHGyXrZZhW" +
+    "7filhLAdTGjDJHdtMr3/K0dJdMJ77kXDqdo4bN7LyJvaeO0ipVhHe4m1iWdq5EITjbLHCQE" +
+    "LL8Wiy/l8Y+ZFzG4s/5JI/pyUcQx1QOs2hgKNe2NAgMBAAGjHTAbMAwGA1UdEwQFMAMBAf8" +
+    "wCwYDVR0PBAQDAgEGMA0GCSqGSIb3DQEBCwUAA4IBAQCDjewR53YLc3HzZKugRDbQVxjJNI" +
+    "LW6fSIyW9dSglYcWh6aiOK9cZFVtzRWYEYkIlicAyTiPw34bXzxU1cK6sCSmBR+UTXbRPGb" +
+    "4OOy3MRaoF1m3jxwnPkQwxezDiqJTydCbYcBu0sKwURAZOd5QK922MsOsnrLjNlpRDmuH0V" +
+    "Fhb5uN2I5mM3NvMnP2Or19O1Bk//iGD6AyJfiZFcii+FsDrJhbzw6lakEV7O/EnD0kk2l7I" +
+    "0VMtg1xZBbEw7P6+V9zz5cAzaaq7EB0mCE+jJckSzSETBN+7lyVD8gwmHYxxZfPnUM/yvPb" +
+    "MU9L3xWD/z6HHwO6r+9m7BT+2pHjBCMIIC3TCCAcWgAwIBAgIUa0X7/7DlTaedpgrIJg25i" +
+    "BPOkIMwDQYJKoZIhvcNAQELBQAwEjEQMA4GA1UEAwwHVGVzdCBDQTAiGA8yMDE1MDEwMTAw" +
+    "MDAwMFoYDzIwMjUwMTAxMDAwMDAwWjAcMRowGAYDVQQDDBFUZXN0IEludGVybWVkaWF0ZTC" +
+    "CASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALqIUahEjhbWQf1utogGNhA9PBPZ6u" +
+    "Q1SrTs9WhXbCR7wcclqODYH72xnAabbhqG8mvir1p1a2pkcQh6pVqnRYf3HNUknAJ+zUP8H" +
+    "mnQOCApk6sgw0nk27lMwmtsDu0Vgg/xfq1pGrHTAjqLKkHup3DgDw2N/WYLK7AkkqR9uYhh" +
+    "eZCxV5A90jvF4LhIH6g304hD7ycW2FW3ZlqqfgKQLzp7EIAGJMwcbJetlmFbt+KWEsB1MaM" +
+    "Mkd20yvf8rR0l0wnvuRcOp2jhs3svIm9p47SKlWEd7ibWJZ2rkQhONsscJAQsvxaLL+Xxj5" +
+    "kXMbiz/kkj+nJRxDHVA6zaGAo17Y0CAwEAAaMdMBswDAYDVR0TBAUwAwEB/zALBgNVHQ8EB" +
+    "AMCAQYwDQYJKoZIhvcNAQELBQADggEBAILNZM9yT9ylMpjyi0tXaDORzpHiJ8vEoVKk98bC" +
+    "2BQF0kMEEB547p+Ms8zdJY00Bxe9qigT8rQwKprXq5RvgIZ32QLn/yMPiCp/e6zBdsx77Tk" +
+    "fmnSnxvPi+0nlA+eM8JYN0UST4vWD4vPPX9GgZDVoGQTiF3hUivJ5R8sHb/ozcSukMKQQ22" +
+    "+AIU7w6wyAIbCAG7Pab4k2XFAeEnUZsl9fCym5jsPN9Pnv9rlBi6h8shHw1R2ROXjgxubji" +
+    "Mr3B456vFTJImLJjyA1iTSlr/+VXGUYg6Z0/HYnsO00+8xUKM71dPxGAfIFNaSscpykrGFL" +
+    "vocT/kym6r8galxCJUowggM1MIICHaADAgECAhQVzotp3WBWjtrWSBnwzntPfhsirTANBgk" +
+    "qhkiG9w0BAQsFADAcMRowGAYDVQQDDBFUZXN0IEludGVybWVkaWF0ZTAiGA8yMDE2MTEyNz" +
+    "AwMDAwMFoYDzIwMTkwMjA1MDAwMDAwWjAsMSowKAYDVQQDDCFUZXN0IEVuZC1lbnRpdHkgd" +
+    "2l0aCBJbnRlcm1lZGlhdGUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6iFGo" +
+    "RI4W1kH9braIBjYQPTwT2erkNUq07PVoV2wke8HHJajg2B+9sZwGm24ahvJr4q9adWtqZHE" +
+    "IeqVap0WH9xzVJJwCfs1D/B5p0DggKZOrIMNJ5Nu5TMJrbA7tFYIP8X6taRqx0wI6iypB7q" +
+    "dw4A8Njf1mCyuwJJKkfbmIYXmQsVeQPdI7xeC4SB+oN9OIQ+8nFthVt2Zaqn4CkC86exCAB" +
+    "iTMHGyXrZZhW7filhLAdTGjDJHdtMr3/K0dJdMJ77kXDqdo4bN7LyJvaeO0ipVhHe4m1iWd" +
+    "q5EITjbLHCQELL8Wiy/l8Y+ZFzG4s/5JI/pyUcQx1QOs2hgKNe2NAgMBAAGjWzBZMCMGA1U" +
+    "dEQQcMBqCCWxvY2FsaG9zdIINKi5leGFtcGxlLmNvbTAyBggrBgEFBQcBAQQmMCQwIgYIKw" +
+    "YBBQUHMAGGFmh0dHA6Ly9sb2NhbGhvc3Q6ODg4OC8wDQYJKoZIhvcNAQELBQADggEBACPsk" +
+    "NbuRLteV4TnhgK1lnMk0v6Y7qVo7NtlRc+O5obGVkwvbt4CV225ZL+IEamvykjXr+a3ajWb" +
+    "ai3v+nVB4RvijEcx5dB2VVZC61vWt1qeKPAqr2IWazi7TXmUUL3KO3zRfkeUyKUa9eKeZa4" +
+    "zdB33dT/a/XYDRj5dwg5H6C0EU3x2hYpp1ZVJ8ECUa2atWi7sYc1xcbeVkKYlLF/8PcsIEs" +
+    "qC1o/O6WORvJn0dVIJeIkmgOi1DYmfKea+YE17yeEOfH/JmjyvxTgkHYDajjOBHUQeV/H9D" +
+    "urmv2hM99GHlmMqf0WwOfhUSKvED1sTk94l4VLgNDyZNyZap2BSLqIxAAAAAAAAAA==";
+  let certListWithIntermediate = build_cert_chain(["ocspEEWithIntermediate",
+                                                   "test-int", "test-ca"],
+                                                   "ocsp_certs");
+  let pkcs7WithIntermediate = certListWithIntermediate.asPKCS7Blob();
+  equal(btoa(pkcs7WithIntermediate), expectedPKCS7WithIntermediate,
+        "PKCS7 export should work as expected for chain with intermediate");
+}
+
 function test_security_info_serialization(securityInfo, expectedErrorCode) {
   // Serialize the securityInfo to a string
   let serHelper = Cc["@mozilla.org/network/serialization-helper;1"]
                     .getService(Ci.nsISerializationHelper);
   let serialized = serHelper.serializeToString(securityInfo);
 
   // Deserialize from the string and compare to the original object
   let deserialized = serHelper.deserializeObject(serialized);
@@ -67,16 +201,21 @@ function run_test() {
   });
 
   // Test serialization of nsIX509CertList
   add_test(function() {
     test_cert_list_serialization();
     run_next_test();
   });
 
+  add_test(function() {
+    test_cert_pkcs7_export();
+    run_next_test();
+  });
+
   // Test successful connection (failedCertChain should be null)
   add_connection_test(
     // re-use pinning certs (keeler)
     "good.include-subdomains.pinning.example.com", PRErrorCodeSuccess, null,
     function withSecurityInfo(aTransportSecurityInfo) {
       aTransportSecurityInfo.QueryInterface(Ci.nsITransportSecurityInfo);
       test_security_info_serialization(aTransportSecurityInfo, 0);
       equal(aTransportSecurityInfo.failedCertChain, null,