Bug 1317951, part 2 - Certificate Transparency - basic support for disqualified logs. r=keeler draft
authorSergei Chernov <sergei.cv@ndivi.com>
Tue, 29 Nov 2016 22:51:46 +0200
changeset 446293 3d6dbfa4d0a6a24dbd8907f42071943895ba068a
parent 445518 d5590733c87c3a8a525eb29dabfdb9be2822a0a6
child 538747 1a2b3969e824e60270ce415509a7af7668e529c5
push id37746
push usersergei.cv@ndivi.com
push dateThu, 01 Dec 2016 06:48:42 +0000
reviewerskeeler
bugs1317951
milestone53.0a1
Bug 1317951, part 2 - Certificate Transparency - basic support for disqualified logs. r=keeler MozReview-Commit-ID: 4y2JYFnO9Xm
security/certverifier/CTKnownLogs.h
security/certverifier/CTLog.h
security/certverifier/CTLogVerifier.cpp
security/certverifier/CTLogVerifier.h
security/certverifier/CTVerifyResult.cpp
security/certverifier/CTVerifyResult.h
security/certverifier/CertVerifier.cpp
security/certverifier/MultiLogCTVerifier.cpp
security/certverifier/SignedCertificateTimestamp.h
security/certverifier/moz.build
security/certverifier/tests/gtest/CTLogVerifierTest.cpp
security/certverifier/tests/gtest/CTObjectsExtractorTest.cpp
security/certverifier/tests/gtest/MultiLogCTVerifierTest.cpp
security/manager/ssl/SSLServerCertVerification.cpp
security/manager/ssl/nsSSLStatus.cpp
security/manager/tools/getCTKnownLogs.py
security/manager/tools/log_list.json
toolkit/components/telemetry/Histograms.json
--- a/security/certverifier/CTKnownLogs.h
+++ b/security/certverifier/CTKnownLogs.h
@@ -4,91 +4,121 @@
  * 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/. */
 
 /* This file was automatically generated by getCTKnownLogs.py. */
 
 #ifndef CTKnownLogs_h
 #define CTKnownLogs_h
 
+#include "CTLog.h"
+
 #include <stddef.h>
 
-struct CTLogInfo {
-  const char* const logName;
-  const char* const logUrl;
-  const char* const logKey;
-  const size_t logKeyLength;
+struct CTLogInfo
+{
+  const char* const name;
+  // Index within kCTLogOperatorList.
+  const mozilla::ct::CTLogStatus status;
+  // 0 for qualified logs, disqualification time for disqualified logs
+  // (in milliseconds, measured since the epoch, ignoring leap seconds).
+  const uint64_t disqualificationTime;
+  const size_t operatorIndex;
+  const char* const key;
+  const size_t keyLength;
+};
+
+struct CTLogOperatorInfo
+{
+  const char* const name;
+  const mozilla::ct::CTLogOperatorId id;
 };
 
 const CTLogInfo kCTLogList[] = {
   { "Google 'Pilot' log",
-    "https://ct.googleapis.com/pilot/",
+    mozilla::ct::CTLogStatus::Included,
+    0, // no disqualification time
+    0, // operated by Google
     "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
     "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x7d\xa8\x4b\x12\x29\x80\xa3\x3d\xad"
     "\xd3\x5a\x77\xb8\xcc\xe2\x88\xb3\xa5\xfd\xf1\xd3\x0c\xcd\x18\x0c\xe8\x41"
     "\x46\xe8\x81\x01\x1b\x15\xe1\x4b\xf1\x1b\x62\xdd\x36\x0a\x08\x18\xba\xed"
     "\x0b\x35\x84\xd0\x9e\x40\x3c\x2d\x9e\x9b\x82\x65\xbd\x1f\x04\x10\x41\x4c"
     "\xa0",
     91 },
   { "Google 'Aviator' log",
-    "https://ct.googleapis.com/aviator/",
+    mozilla::ct::CTLogStatus::Included,
+    0, // no disqualification time
+    0, // operated by Google
     "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
     "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\xd7\xf4\xcc\x69\xb2\xe4\x0e\x90\xa3"
     "\x8a\xea\x5a\x70\x09\x4f\xef\x13\x62\xd0\x8d\x49\x60\xff\x1b\x40\x50\x07"
     "\x0c\x6d\x71\x86\xda\x25\x49\x8d\x65\xe1\x08\x0d\x47\x34\x6b\xbd\x27\xbc"
     "\x96\x21\x3e\x34\xf5\x87\x76\x31\xb1\x7f\x1d\xc9\x85\x3b\x0d\xf7\x1f\x3f"
     "\xe9",
     91 },
   { "DigiCert Log Server",
-    "https://ct1.digicert-ct.com/log/",
+    mozilla::ct::CTLogStatus::Included,
+    0, // no disqualification time
+    1, // operated by DigiCert
     "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
     "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x02\x46\xc5\xbe\x1b\xbb\x82\x40\x16"
     "\xe8\xc1\xd2\xac\x19\x69\x13\x59\xf8\xf8\x70\x85\x46\x40\xb9\x38\xb0\x23"
     "\x82\xa8\x64\x4c\x7f\xbf\xbb\x34\x9f\x4a\x5f\x28\x8a\xcf\x19\xc4\x00\xf6"
     "\x36\x06\x93\x65\xed\x4c\xf5\xa9\x21\x62\x5a\xd8\x91\xeb\x38\x24\x40\xac"
     "\xe8",
     91 },
   { "Google 'Rocketeer' log",
-    "https://ct.googleapis.com/rocketeer/",
+    mozilla::ct::CTLogStatus::Included,
+    0, // no disqualification time
+    0, // operated by Google
     "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
     "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x20\x5b\x18\xc8\x3c\xc1\x8b\xb3\x31"
     "\x08\x00\xbf\xa0\x90\x57\x2b\xb7\x47\x8c\x6f\xb5\x68\xb0\x8e\x90\x78\xe9"
     "\xa0\x73\xea\x4f\x28\x21\x2e\x9c\xc0\xf4\x16\x1b\xaa\xf9\xd5\xd7\xa9\x80"
     "\xc3\x4e\x2f\x52\x3c\x98\x01\x25\x46\x24\x25\x28\x23\x77\x2d\x05\xc2\x40"
     "\x7a",
     91 },
   { "Certly.IO log",
-    "https://log.certly.io/",
+    mozilla::ct::CTLogStatus::Disqualified,
+    1460678400000, // Date.parse("2016-04-15T00:00:00Z")
+    2, // operated by Certly
     "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
     "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x0b\x23\xcb\x85\x62\x98\x61\x48\x04"
     "\x73\xeb\x54\x5d\xf3\xd0\x07\x8c\x2d\x19\x2d\x8c\x36\xf5\xeb\x8f\x01\x42"
     "\x0a\x7c\x98\x26\x27\xc1\xb5\xdd\x92\x93\xb0\xae\xf8\x9b\x3d\x0c\xd8\x4c"
     "\x4e\x1d\xf9\x15\xfb\x47\x68\x7b\xba\x66\xb7\x25\x9c\xd0\x4a\xc2\x66\xdb"
     "\x48",
     91 },
   { "Izenpe log",
-    "https://ct.izenpe.com/",
+    mozilla::ct::CTLogStatus::Disqualified,
+    1464566400000, // Date.parse("2016-05-30T00:00:00Z")
+    3, // operated by Izenpe
     "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
     "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x27\x64\x39\x0c\x2d\xdc\x50\x18\xf8"
     "\x21\x00\xa2\x0e\xed\x2c\xea\x3e\x75\xba\x9f\x93\x64\x09\x00\x11\xc4\x11"
     "\x17\xab\x5c\xcf\x0f\x74\xac\xb5\x97\x90\x93\x00\x5b\xb8\xeb\xf7\x27\x3d"
     "\xd9\xb2\x0a\x81\x5f\x2f\x0d\x75\x38\x94\x37\x99\x1e\xf6\x07\x76\xe0\xee"
     "\xbe",
     91 },
   { "Symantec log",
-    "https://ct.ws.symantec.com/",
+    mozilla::ct::CTLogStatus::Included,
+    0, // no disqualification time
+    4, // operated by Symantec
     "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
     "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x96\xea\xac\x1c\x46\x0c\x1b\x55\xdc"
     "\x0d\xfc\xb5\x94\x27\x46\x57\x42\x70\x3a\x69\x18\xe2\xbf\x3b\xc4\xdb\xab"
     "\xa0\xf4\xb6\x6c\xc0\x53\x3f\x4d\x42\x10\x33\xf0\x58\x97\x8f\x6b\xbe\x72"
     "\xf4\x2a\xec\x1c\x42\xaa\x03\x2f\x1a\x7e\x28\x35\x76\x99\x08\x3d\x21\x14"
     "\x86",
     91 },
   { "Venafi log",
-    "https://ctlog.api.venafi.com/",
+    mozilla::ct::CTLogStatus::Included,
+    0, // no disqualification time
+    5, // operated by Venafi
     "\x30\x82\x01\x22\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05"
     "\x00\x03\x82\x01\x0f\x00\x30\x82\x01\x0a\x02\x82\x01\x01\x00\xa2\x5a\x48"
     "\x1f\x17\x52\x95\x35\xcb\xa3\x5b\x3a\x1f\x53\x82\x76\x94\xa3\xff\x80\xf2"
     "\x1c\x37\x3c\xc0\xb1\xbd\xc1\x59\x8b\xab\x2d\x65\x93\xd7\xf3\xe0\x04\xd5"
     "\x9a\x6f\xbf\xd6\x23\x76\x36\x4f\x23\x99\xcb\x54\x28\xad\x8c\x15\x4b\x65"
     "\x59\x76\x41\x4a\x9c\xa6\xf7\xb3\x3b\x7e\xb1\xa5\x49\xa4\x17\x51\x6c\x80"
     "\xdc\x2a\x90\x50\x4b\x88\x24\xe9\xa5\x12\x32\x93\x04\x48\x90\x02\xfa\x5f"
     "\x0e\x30\x87\x8e\x55\x76\x05\xee\x2a\x4c\xce\xa3\x6a\x69\x09\x6e\x25\xad"
@@ -98,26 +128,30 @@ const CTLogInfo kCTLogList[] = {
     "\x5b\xe8\x81\xcd\xfd\x92\x68\xe7\xf3\x06\xf0\xe9\x72\x84\xee\x01\xa5\xb1"
     "\xd8\x33\xda\xce\x83\xa5\xdb\xc7\xcf\xd6\x16\x7e\x90\x75\x18\xbf\x16\xdc"
     "\x32\x3b\x6d\x8d\xab\x82\x17\x1f\x89\x20\x8d\x1d\x9a\xe6\x4d\x23\x08\xdf"
     "\x78\x6f\xc6\x05\xbf\x5f\xae\x94\x97\xdb\x5f\x64\xd4\xee\x16\x8b\xa3\x84"
     "\x6c\x71\x2b\xf1\xab\x7f\x5d\x0d\x32\xee\x04\xe2\x90\xec\x41\x9f\xfb\x39"
     "\xc1\x02\x03\x01\x00\x01",
     294 },
   { "Symantec 'Vega' log",
-    "https://vega.ws.symantec.com/",
+    mozilla::ct::CTLogStatus::Included,
+    0, // no disqualification time
+    4, // operated by Symantec
     "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
     "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\xea\x95\x9e\x02\xff\xee\xf1\x33\x6d"
     "\x4b\x87\xbc\xcd\xfd\x19\x17\x62\xff\x94\xd3\xd0\x59\x07\x3f\x02\x2d\x1c"
     "\x90\xfe\xc8\x47\x30\x3b\xf1\xdd\x0d\xb8\x11\x0c\x5d\x1d\x86\xdd\xab\xd3"
     "\x2b\x46\x66\xfb\x6e\x65\xb7\x3b\xfd\x59\x68\xac\xdf\xa6\xf8\xce\xd2\x18"
     "\x4d",
     91 },
   { "CNNIC CT log",
-    "https://ctserver.cnnic.cn/",
+    mozilla::ct::CTLogStatus::Included,
+    0, // no disqualification time
+    6, // operated by CNNIC
     "\x30\x82\x01\x22\x30\x0d\x06\x09\x2a\x86\x48\x86\xf7\x0d\x01\x01\x01\x05"
     "\x00\x03\x82\x01\x0f\x00\x30\x82\x01\x0a\x02\x82\x01\x01\x00\xbf\xb5\x08"
     "\x61\x9a\x29\x32\x04\xd3\x25\x63\xe9\xd8\x85\xe1\x86\xe0\x1f\xd6\x5e\x9a"
     "\xf7\x33\x3b\x80\x1b\xe7\xb6\x3e\x5f\x2d\xa1\x66\xf6\x95\x4a\x84\xa6\x21"
     "\x56\x79\xe8\xf7\x85\xee\x5d\xe3\x7c\x12\xc0\xe0\x89\x22\x09\x22\x3e\xba"
     "\x16\x95\x06\xbd\xa8\xb9\xb1\xa9\xb2\x7a\xd6\x61\x2e\x87\x11\xb9\x78\x40"
     "\x89\x75\xdb\x0c\xdc\x90\xe0\xa4\x79\xd6\xd5\x5e\x6e\xd1\x2a\xdb\x34\xf4"
     "\x99\x3f\x65\x89\x3b\x46\xc2\x29\x2c\x15\x07\x1c\xc9\x4b\x1a\x54\xf8\x6c"
@@ -127,28 +161,44 @@ const CTLogInfo kCTLogList[] = {
     "\xb2\xe5\x9a\x6c\x0d\xc5\x1c\xa5\x8b\xf7\x3f\x30\xaf\xb9\x01\x91\xb7\x69"
     "\x12\x12\xe5\x83\x61\xfe\x34\x00\xbe\xf6\x71\x8a\xc7\xeb\x50\x92\xe8\x59"
     "\xfe\x15\x91\xeb\x96\x97\xf8\x23\x54\x3f\x2d\x8e\x07\xdf\xee\xda\xb3\x4f"
     "\xc8\x3c\x9d\x6f\xdf\x3c\x2c\x43\x57\xa1\x47\x0c\x91\x04\xf4\x75\x4d\xda"
     "\x89\x81\xa4\x14\x06\x34\xb9\x98\xc3\xda\xf1\xfd\xed\x33\x36\xd3\x16\x2d"
     "\x35\x02\x03\x01\x00\x01",
     294 },
   { "WoSign log",
-    "https://ctlog.wosign.com/",
+    mozilla::ct::CTLogStatus::Included,
+    0, // no disqualification time
+    7, // operated by WoSign
     "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
     "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\xcc\x11\x88\x7b\x2d\x66\xcb\xae\x8f"
     "\x4d\x30\x66\x27\x19\x25\x22\x93\x21\x46\xb4\x2f\x01\xd3\xc6\xf9\x2b\xd5"
     "\xc8\xba\x73\x9b\x06\xa2\xf0\x8a\x02\x9c\xd0\x6b\x46\x18\x30\x85\xba\xe9"
     "\x24\x8b\x0e\xd1\x5b\x70\x28\x0c\x7e\xf1\x3a\x45\x7f\x5a\xf3\x82\x42\x60"
     "\x31",
     91 },
   { "StartCom log",
-    "https://ct.startssl.com/",
+    mozilla::ct::CTLogStatus::Included,
+    0, // no disqualification time
+    8, // operated by StartCom
     "\x30\x59\x30\x13\x06\x07\x2a\x86\x48\xce\x3d\x02\x01\x06\x08\x2a\x86\x48"
     "\xce\x3d\x03\x01\x07\x03\x42\x00\x04\x48\xf3\x59\xf3\xf6\x05\x18\xd3\xdb"
     "\xb2\xed\x46\x7e\xcf\xc8\x11\xb5\x57\xb1\xa8\xd6\x4c\xe6\x9f\xb7\x4a\x1a"
     "\x14\x86\x43\xa9\x48\xb0\xcb\x5a\x3f\x3c\x4a\xca\xdf\xc4\x82\x14\x55\x9a"
     "\xf8\xf7\x8e\x40\x55\xdc\xf4\xd2\xaf\xea\x75\x74\xfb\x4e\x7f\x60\x86\x2e"
     "\x51",
     91 }
 };
 
+const CTLogOperatorInfo kCTLogOperatorList[] = {
+  { "Google", 0 },
+  { "DigiCert", 1 },
+  { "Certly", 2 },
+  { "Izenpe", 3 },
+  { "Symantec", 4 },
+  { "Venafi", 5 },
+  { "CNNIC", 7 },
+  { "WoSign", 8 },
+  { "StartCom", 9 }
+};
+
 #endif // CTKnownLogs_h
new file mode 100644
--- /dev/null
+++ b/security/certverifier/CTLog.h
@@ -0,0 +1,34 @@
+/* -*- 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/. */
+
+#ifndef CTLog_h
+#define CTLog_h
+
+#include <stdint.h>
+
+namespace mozilla { namespace ct {
+
+// Signed integer sufficient to store the numeric ID of CT log operators
+// as assigned at https://www.certificate-transparency.org/known-logs .
+// The assigned IDs are 0-based positive integers, so you can use special
+// values (such as -1) to indicate a "null" or unknown log ID.
+typedef int16_t CTLogOperatorId;
+
+// Current status of a CT log in regard to its inclusion in the
+// Known Logs List such as https://www.certificate-transparency.org/known-logs
+enum class CTLogStatus
+{
+  // Status unknown or unavailable.
+  Unknown,
+  // Included in the list of known logs.
+  Included,
+  // Previously included, but disqualified at some point of time.
+  Disqualified,
+};
+
+} } // namespace mozilla::ct
+
+#endif // CTLog_h
--- a/security/certverifier/CTLogVerifier.cpp
+++ b/security/certverifier/CTLogVerifier.cpp
@@ -1,16 +1,18 @@
 /* -*- 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 "CTLogVerifier.h"
 
+#include <stdint.h>
+
 #include "CTSerialization.h"
 #include "hasht.h"
 #include "mozilla/ArrayUtils.h"
 #include "mozilla/Assertions.h"
 #include "pkix/pkixnss.h"
 #include "pkixutil.h"
 
 namespace mozilla { namespace ct {
@@ -115,22 +117,43 @@ public:
   }
 
   DigitallySigned::SignatureAlgorithm mSignatureAlgorithm;
 };
 
 
 CTLogVerifier::CTLogVerifier()
   : mSignatureAlgorithm(DigitallySigned::SignatureAlgorithm::Anonymous)
+  , mOperatorId(-1)
+  , mDisqualified(false)
+  , mDisqualificationTime(UINT64_MAX)
 {
 }
 
 Result
-CTLogVerifier::Init(Input subjectPublicKeyInfo)
+CTLogVerifier::Init(Input subjectPublicKeyInfo,
+                    CTLogOperatorId operatorId,
+                    CTLogStatus logStatus,
+                    uint64_t disqualificationTime)
 {
+  switch (logStatus) {
+    case CTLogStatus::Included:
+      mDisqualified = false;
+      mDisqualificationTime = UINT64_MAX;
+      break;
+    case CTLogStatus::Disqualified:
+      mDisqualified = true;
+      mDisqualificationTime = disqualificationTime;
+      break;
+    case CTLogStatus::Unknown:
+    default:
+      MOZ_ASSERT_UNREACHABLE("Unsupported CTLogStatus");
+      return Result::FATAL_ERROR_INVALID_ARGS;
+  }
+
   SignatureParamsTrustDomain trustDomain;
   Result rv = CheckSubjectPublicKeyInfo(subjectPublicKeyInfo, trustDomain,
                                         EndEntityOrCA::MustBeEndEntity);
   if (rv != Success) {
     return rv;
   }
   mSignatureAlgorithm = trustDomain.mSignatureAlgorithm;
 
@@ -143,16 +166,17 @@ CTLogVerifier::Init(Input subjectPublicK
     return Result::FATAL_ERROR_NO_MEMORY;
   }
   rv = DigestBufNSS(subjectPublicKeyInfo, DigestAlgorithm::sha256,
                     mKeyId.begin(), mKeyId.length());
   if (rv != Success) {
     return rv;
   }
 
+  mOperatorId = operatorId;
   return Success;
 }
 
 Result
 CTLogVerifier::Verify(const LogEntry& entry,
                       const SignedCertificateTimestamp& sct)
 {
   if (mKeyId.empty() || sct.logId != mKeyId) {
--- a/security/certverifier/CTLogVerifier.h
+++ b/security/certverifier/CTLogVerifier.h
@@ -2,16 +2,17 @@
 /* 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/. */
 
 #ifndef CTLogVerifier_h
 #define CTLogVerifier_h
 
+#include "CTLog.h"
 #include "pkix/Input.h"
 #include "pkix/pkix.h"
 #include "SignedCertificateTimestamp.h"
 #include "SignedTreeHead.h"
 
 namespace mozilla { namespace ct {
 
 // Verifies Signed Certificate Timestamps (SCTs) provided by a specific log
@@ -21,26 +22,38 @@ namespace mozilla { namespace ct {
 // The verification functions return Success if the provided SCT has passed
 // verification, ERROR_BAD_SIGNATURE if failed verification, or other result
 // on error.
 class CTLogVerifier
 {
 public:
   CTLogVerifier();
 
-  // Initializes the verifier with log-specific information.
+  // Initializes the verifier with log-specific information. Only the public
+  // key is used for verification, other parameters are purely informational.
   // |subjectPublicKeyInfo| is a DER-encoded SubjectPublicKeyInfo.
+  // |operatorId| The numeric ID of the log operator as assigned at
+  // https://www.certificate-transparency.org/known-logs .
+  // |logStatus| Either "Included" or "Disqualified".
+  // |disqualificationTime| Disqualification timestamp (for disqualified logs).
   // An error is returned if |subjectPublicKeyInfo| refers to an unsupported
   // public key.
-  pkix::Result Init(pkix::Input subjectPublicKeyInfo);
+  pkix::Result Init(pkix::Input subjectPublicKeyInfo,
+                    CTLogOperatorId operatorId,
+                    CTLogStatus logStatus,
+                    uint64_t disqualificationTime);
 
   // Returns the log's key ID, which is a SHA256 hash of its public key.
   // See RFC 6962, Section 3.2.
   const Buffer& keyId() const { return mKeyId; }
 
+  CTLogOperatorId operatorId() const { return mOperatorId; }
+  bool isDisqualified() const { return mDisqualified; }
+  uint64_t disqualificationTime() const { return mDisqualificationTime; }
+
   // Verifies that |sct| contains a valid signature for |entry|.
   // |sct| must be signed by the verifier's log.
   pkix::Result Verify(const LogEntry& entry,
                       const SignedCertificateTimestamp& sct);
 
   // Verifies the signature in |sth|.
   // |sth| must be signed by the verifier's log.
   pkix::Result VerifySignedTreeHead(const SignedTreeHead& sth);
@@ -56,13 +69,16 @@ private:
   // Returns Success if passed verification, ERROR_BAD_SIGNATURE if failed
   // verification, or other result on error.
   pkix::Result VerifySignature(pkix::Input data, pkix::Input signature);
   pkix::Result VerifySignature(const Buffer& data, const Buffer& signature);
 
   Buffer mSubjectPublicKeyInfo;
   Buffer mKeyId;
   DigitallySigned::SignatureAlgorithm mSignatureAlgorithm;
+  CTLogOperatorId mOperatorId;
+  bool mDisqualified;
+  uint64_t mDisqualificationTime;
 };
 
 } } // namespace mozilla::ct
 
 #endif // CTLogVerifier_h
--- a/security/certverifier/CTVerifyResult.cpp
+++ b/security/certverifier/CTVerifyResult.cpp
@@ -8,16 +8,18 @@
 
 #include <stdint.h>
 
 namespace mozilla { namespace ct {
 
 VerifiedSCT::VerifiedSCT()
   : status(Status::None)
   , origin(Origin::Unknown)
+  , logOperatorId(-1)
+  , logDisqualificationTime(UINT64_MAX)
 {
 }
 
 void
 CTVerifyResult::Reset()
 {
   verifiedScts.clear();
   decodingErrors = 0;
--- a/security/certverifier/CTVerifyResult.h
+++ b/security/certverifier/CTVerifyResult.h
@@ -2,53 +2,61 @@
 /* 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/. */
 
 #ifndef CTVerifyResult_h
 #define CTVerifyResult_h
 
+#include "CTLog.h"
 #include "mozilla/Vector.h"
 #include "SignedCertificateTimestamp.h"
 
 namespace mozilla { namespace ct {
 
 // Holds a verified Signed Certificate Timestamp along with the verification
 // status (e.g. valid/invalid) and additional information related to the
 // verification.
 struct VerifiedSCT
 {
   VerifiedSCT();
 
   // The original SCT.
   SignedCertificateTimestamp sct;
 
-  enum class Status {
+  enum class Status
+  {
     None,
     // The SCT is from a known log, and the signature is valid.
     Valid,
+    // The SCT is from a known disqualified log, and the signature is valid.
+    // For the disqualification time of the log see |logDisqualificationTime|.
+    ValidFromDisqualifiedLog,
     // The SCT is from an unknown log and can not be verified.
     UnknownLog,
     // The SCT is from a known log, but the signature is invalid.
     InvalidSignature,
     // The SCT signature is valid, but the timestamp is in the future.
     // Such SCTs are considered invalid (see RFC 6962, Section 5.2).
     InvalidTimestamp,
   };
 
-  enum class Origin {
+  enum class Origin
+  {
     Unknown,
     Embedded,
     TLSExtension,
     OCSPResponse,
   };
 
   Status status;
   Origin origin;
+  CTLogOperatorId logOperatorId;
+  uint64_t logDisqualificationTime;
 };
 
 typedef Vector<VerifiedSCT> VerifiedSCTList;
 
 // Holds Signed Certificate Timestamps verification results.
 class CTVerifyResult
 {
 public:
--- a/security/certverifier/CertVerifier.cpp
+++ b/security/certverifier/CertVerifier.cpp
@@ -155,24 +155,27 @@ BuildCertChainForOneKeyUsage(NSSCertDBTr
 
 void
 CertVerifier::LoadKnownCTLogs()
 {
   mCTVerifier = MakeUnique<MultiLogCTVerifier>();
   for (const CTLogInfo& log : kCTLogList) {
     Input publicKey;
     Result rv = publicKey.Init(
-      BitwiseCast<const uint8_t*, const char*>(log.logKey), log.logKeyLength);
+      BitwiseCast<const uint8_t*, const char*>(log.key), log.keyLength);
     if (rv != Success) {
       MOZ_ASSERT_UNREACHABLE("Failed reading a log key for a known CT Log");
       continue;
     }
 
     CTLogVerifier logVerifier;
-    rv = logVerifier.Init(publicKey);
+    const CTLogOperatorInfo& logOperator =
+      kCTLogOperatorList[log.operatorIndex];
+    rv = logVerifier.Init(publicKey, logOperator.id, log.status,
+                          log.disqualificationTime);
     if (rv != Success) {
       MOZ_ASSERT_UNREACHABLE("Failed initializing a known CT Log");
       continue;
     }
 
     rv = mCTVerifier->AddLog(Move(logVerifier));
     if (rv != Success) {
       MOZ_ASSERT_UNREACHABLE("Failed activating a known CT Log");
@@ -264,43 +267,47 @@ CertVerifier::VerifySignedCertificateTim
     MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
             ("SCT verification failed with fatal error %i\n", rv));
     return rv;
   }
 
   if (MOZ_LOG_TEST(gCertVerifierLog, LogLevel::Debug)) {
     size_t validCount = 0;
     size_t unknownLogCount = 0;
+    size_t disqualifiedLogCount = 0;
     size_t invalidSignatureCount = 0;
     size_t invalidTimestampCount = 0;
     for (const VerifiedSCT& verifiedSct : result.verifiedScts) {
       switch (verifiedSct.status) {
         case VerifiedSCT::Status::Valid:
           validCount++;
           break;
+        case VerifiedSCT::Status::ValidFromDisqualifiedLog:
+          disqualifiedLogCount++;
+          break;
         case VerifiedSCT::Status::UnknownLog:
           unknownLogCount++;
           break;
         case VerifiedSCT::Status::InvalidSignature:
           invalidSignatureCount++;
           break;
         case VerifiedSCT::Status::InvalidTimestamp:
           invalidTimestampCount++;
           break;
         case VerifiedSCT::Status::None:
         default:
           MOZ_ASSERT_UNREACHABLE("Unexpected SCT verification status");
       }
     }
     MOZ_LOG(gCertVerifierLog, LogLevel::Debug,
             ("SCT verification result: "
-             "valid=%zu unknownLog=%zu "
+             "valid=%zu unknownLog=%zu disqualifiedLog=%zu "
              "invalidSignature=%zu invalidTimestamp=%zu "
              "decodingErrors=%zu\n",
-             validCount, unknownLogCount,
+             validCount, unknownLogCount, disqualifiedLogCount,
              invalidSignatureCount, invalidTimestampCount,
              result.decodingErrors));
   }
 
   if (ctInfo) {
     ctInfo->processedSCTs = true;
     ctInfo->verifyResult = Move(result);
   }
--- a/security/certverifier/MultiLogCTVerifier.cpp
+++ b/security/certverifier/MultiLogCTVerifier.cpp
@@ -150,16 +150,18 @@ MultiLogCTVerifier::VerifySingleSCT(Sign
   }
 
   if (!matchingLog) {
     // SCT does not match any known log.
     return StoreVerifiedSct(result, Move(verifiedSct),
                             VerifiedSCT::Status::UnknownLog);
   }
 
+  verifiedSct.logOperatorId = matchingLog->operatorId();
+
   if (!matchingLog->SignatureParametersMatch(verifiedSct.sct.signature)) {
     // SCT signature parameters do not match the log's.
     return StoreVerifiedSct(result, Move(verifiedSct),
                             VerifiedSCT::Status::InvalidSignature);
   }
 
   Result rv = matchingLog->Verify(expectedEntry, verifiedSct.sct);
   if (rv != Success) {
@@ -178,14 +180,22 @@ MultiLogCTVerifier::VerifySingleSCT(Sign
   // it does not matter.
   Time sctTime =
     TimeFromEpochInSeconds((verifiedSct.sct.timestamp + 999u) / 1000u);
   if (sctTime > time) {
     return StoreVerifiedSct(result, Move(verifiedSct),
                             VerifiedSCT::Status::InvalidTimestamp);
   }
 
-  // SCT verified ok.
+  // SCT verified ok, see if the log is qualified. Since SCTs from
+  // disqualified logs are treated as valid under certain circumstances (see
+  // the CT Policy), the log qualification check must be the last one we do.
+  if (matchingLog->isDisqualified()) {
+    verifiedSct.logDisqualificationTime = matchingLog->disqualificationTime();
+    return StoreVerifiedSct(result, Move(verifiedSct),
+                            VerifiedSCT::Status::ValidFromDisqualifiedLog);
+  }
+
   return StoreVerifiedSct(result, Move(verifiedSct),
                           VerifiedSCT::Status::Valid);
 }
 
 } } // namespace mozilla::ct
--- a/security/certverifier/SignedCertificateTimestamp.h
+++ b/security/certverifier/SignedCertificateTimestamp.h
@@ -82,17 +82,16 @@ struct SignedCertificateTimestamp
   Buffer logId;
   // "timestamp" is the current time in milliseconds, measured since the epoch,
   // ignoring leap seconds. See RFC 6962, Section 3.2.
   uint64_t timestamp;
   Buffer extensions;
   DigitallySigned signature;
 };
 
-
 inline pkix::Result BufferToInput(const Buffer& buffer, pkix::Input& input)
 {
   return input.Init(buffer.begin(), buffer.length());
 }
 
 inline pkix::Result InputToBuffer(pkix::Input input, Buffer& buffer)
 {
   buffer.clear();
--- a/security/certverifier/moz.build
+++ b/security/certverifier/moz.build
@@ -2,16 +2,17 @@
 # vim: set filetype=python:
 # 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/.
 
 EXPORTS += [
     'BRNameMatchingPolicy.h',
     'CertVerifier.h',
+    'CTLog.h',
     'CTVerifyResult.h',
     'OCSPCache.h',
     'SignedCertificateTimestamp.h',
     'SignedTreeHead.h',
 ]
 
 UNIFIED_SOURCES += [
     'BRNameMatchingPolicy.cpp',
--- a/security/certverifier/tests/gtest/CTLogVerifierTest.cpp
+++ b/security/certverifier/tests/gtest/CTLogVerifierTest.cpp
@@ -17,17 +17,20 @@ using namespace pkix;
 class CTLogVerifierTest : public ::testing::Test
 {
 public:
   void SetUp() override
   {
     // Does nothing if NSS is already initialized.
     MOZ_RELEASE_ASSERT(NSS_NoDB_Init(nullptr) == SECSuccess);
 
-    ASSERT_EQ(Success, mLog.Init(InputForBuffer(GetTestPublicKey())));
+    ASSERT_EQ(Success, mLog.Init(InputForBuffer(GetTestPublicKey()),
+                                 -1 /*operator id*/,
+                                 CTLogStatus::Included,
+                                 0 /*disqualification time*/));
     ASSERT_EQ(GetTestPublicKeyId(), mLog.keyId());
   }
 
 protected:
   CTLogVerifier mLog;
 };
 
 TEST_F(CTLogVerifierTest, VerifiesCertSCT)
@@ -118,12 +121,15 @@ TEST_F(CTLogVerifierTest, DoesNotVerifyI
 
 // Test that excess data after the public key is rejected.
 TEST_F(CTLogVerifierTest, ExcessDataInPublicKey)
 {
   Buffer key = GetTestPublicKey();
   MOZ_RELEASE_ASSERT(key.append("extra", 5));
 
   CTLogVerifier log;
-  EXPECT_NE(Success, log.Init(InputForBuffer(key)));
+  EXPECT_NE(Success, log.Init(InputForBuffer(key),
+                              -1 /*operator id*/,
+                              CTLogStatus::Included,
+                              0 /*disqualification time*/));
 }
 
 } }  // namespace mozilla::ct
--- a/security/certverifier/tests/gtest/CTObjectsExtractorTest.cpp
+++ b/security/certverifier/tests/gtest/CTObjectsExtractorTest.cpp
@@ -24,17 +24,20 @@ public:
     MOZ_RELEASE_ASSERT(NSS_NoDB_Init(nullptr) == SECSuccess);
 
     mTestCert = GetDEREncodedX509Cert();
     mEmbeddedCert = GetDEREncodedTestEmbeddedCert();
     mCaCert = GetDEREncodedCACert();
     mCaCertSPKI = ExtractCertSPKI(mCaCert);
 
     Buffer logPublicKey = GetTestPublicKey();
-    ASSERT_EQ(Success, mLog.Init(InputForBuffer(logPublicKey)));
+    ASSERT_EQ(Success, mLog.Init(InputForBuffer(logPublicKey),
+                                 -1 /*operator id*/,
+                                 CTLogStatus::Included,
+                                 0 /*disqualification time*/));
   }
 
 protected:
   Buffer mTestCert;
   Buffer mEmbeddedCert;
   Buffer mCaCert;
   Buffer mCaCertSPKI;
   CTLogVerifier mLog;
--- a/security/certverifier/tests/gtest/MultiLogCTVerifierTest.cpp
+++ b/security/certverifier/tests/gtest/MultiLogCTVerifierTest.cpp
@@ -21,25 +21,29 @@ namespace mozilla { namespace ct {
 
 using namespace mozilla::pkix;
 
 class MultiLogCTVerifierTest : public ::testing::Test
 {
 public:
   MultiLogCTVerifierTest()
     : mNow(Time::uninitialized)
+    , mLogOperatorID(123)
   {}
 
   void SetUp() override
   {
     // Does nothing if NSS is already initialized.
     MOZ_RELEASE_ASSERT(NSS_NoDB_Init(nullptr) == SECSuccess);
 
     CTLogVerifier log;
-    ASSERT_EQ(Success, log.Init(InputForBuffer(GetTestPublicKey())));
+    ASSERT_EQ(Success, log.Init(InputForBuffer(GetTestPublicKey()),
+                                mLogOperatorID,
+                                CTLogStatus::Included,
+                                0 /*disqualification time*/));
     ASSERT_EQ(Success, mVerifier.AddLog(Move(log)));
 
     mTestCert = GetDEREncodedX509Cert();
     mEmbeddedCert = GetDEREncodedTestEmbeddedCert();
     mCaCert = GetDEREncodedCACert();
     mCaCertSPKI = ExtractCertSPKI(mCaCert);
     mIntermediateCert = GetDEREncodedIntermediateCert();
     mIntermediateCertSPKI = ExtractCertSPKI(mIntermediateCert);
@@ -50,16 +54,17 @@ public:
 
   void CheckForSingleValidSCTInResult(const CTVerifyResult& result,
                                       VerifiedSCT::Origin origin)
   {
     EXPECT_EQ(0U, result.decodingErrors);
     ASSERT_EQ(1U, result.verifiedScts.length());
     EXPECT_EQ(VerifiedSCT::Status::Valid, result.verifiedScts[0].status);
     EXPECT_EQ(origin, result.verifiedScts[0].origin);
+    EXPECT_EQ(mLogOperatorID, result.verifiedScts[0].logOperatorId);
   }
 
   // Writes an SCTList containing a single |sct| into |output|.
   void EncodeSCTListForTesting(Input sct, Buffer& output)
   {
     Vector<Input> list;
     ASSERT_TRUE(list.append(Move(sct)));
     ASSERT_EQ(Success, EncodeSCTList(list, output));
@@ -93,16 +98,17 @@ protected:
   MultiLogCTVerifier mVerifier;
   Buffer mTestCert;
   Buffer mEmbeddedCert;
   Buffer mCaCert;
   Buffer mCaCertSPKI;
   Buffer mIntermediateCert;
   Buffer mIntermediateCertSPKI;
   Time mNow;
+  CTLogOperatorId mLogOperatorID;
 };
 
 // Test that an embedded SCT can be extracted and the extracted SCT contains
 // the expected data. This tests the ExtractEmbeddedSCTList function from
 // CTTestUtils.h that other tests here rely upon.
 TEST_F(MultiLogCTVerifierTest, ExtractEmbeddedSCT)
 {
   SignedCertificateTimestamp sct;
@@ -218,9 +224,37 @@ TEST_F(MultiLogCTVerifierTest, Identifie
                              Input(), Input(), InputForBuffer(sctList),
                              mNow, result));
 
   EXPECT_EQ(0U, result.decodingErrors);
   ASSERT_EQ(1U, result.verifiedScts.length());
   EXPECT_EQ(VerifiedSCT::Status::UnknownLog, result.verifiedScts[0].status);
 }
 
+TEST_F(MultiLogCTVerifierTest, IdentifiesSCTFromDisqualifiedLog)
+{
+  MultiLogCTVerifier verifier;
+  CTLogVerifier log;
+  const uint64_t disqualificationTime = 12345u;
+  ASSERT_EQ(Success, log.Init(InputForBuffer(GetTestPublicKey()),
+    mLogOperatorID, CTLogStatus::Disqualified, disqualificationTime));
+  ASSERT_EQ(Success, verifier.AddLog(Move(log)));
+
+  Buffer sct(GetTestSignedCertificateTimestamp());
+  Buffer sctList;
+  EncodeSCTListForTesting(InputForBuffer(sct), sctList);
+
+  CTVerifyResult result;
+  ASSERT_EQ(Success,
+            verifier.Verify(InputForBuffer(mTestCert), Input(),
+                            Input(), Input(), InputForBuffer(sctList),
+                            mNow, result));
+
+  EXPECT_EQ(0U, result.decodingErrors);
+  ASSERT_EQ(1U, result.verifiedScts.length());
+  EXPECT_EQ(VerifiedSCT::Status::ValidFromDisqualifiedLog,
+            result.verifiedScts[0].status);
+  EXPECT_EQ(disqualificationTime,
+            result.verifiedScts[0].logDisqualificationTime);
+  EXPECT_EQ(mLogOperatorID, result.verifiedScts[0].logOperatorId);
+}
+
 } } // namespace mozilla::ct
--- a/security/manager/ssl/SSLServerCertVerification.cpp
+++ b/security/manager/ssl/SSLServerCertVerification.cpp
@@ -1256,16 +1256,19 @@ GatherTelemetryForSingleSCT(const ct::Ve
       verificationStatus = 2;
       break;
     case ct::VerifiedSCT::Status::InvalidSignature:
       verificationStatus = 3;
       break;
     case ct::VerifiedSCT::Status::InvalidTimestamp:
       verificationStatus = 4;
       break;
+    case ct::VerifiedSCT::Status::ValidFromDisqualifiedLog:
+      verificationStatus = 5;
+      break;
     default:
       MOZ_ASSERT_UNREACHABLE("Unexpected VerifiedSCT::Status type");
   }
   Telemetry::Accumulate(Telemetry::SSL_SCTS_VERIFICATION_STATUS,
                         verificationStatus);
 }
 
 void
--- a/security/manager/ssl/nsSSLStatus.cpp
+++ b/security/manager/ssl/nsSSLStatus.cpp
@@ -350,16 +350,17 @@ nsSSLStatus::SetCertificateTransparencyI
   bool hasUnknownLogSCTs = false;
   bool hasInvalidSCTs = false;
   for (const VerifiedSCT& verifiedSct : info.verifyResult.verifiedScts) {
     switch (verifiedSct.status) {
       case VerifiedSCT::Status::Valid:
         hasValidSCTs = true;
         break;
       case VerifiedSCT::Status::UnknownLog:
+      case VerifiedSCT::Status::ValidFromDisqualifiedLog:
         hasUnknownLogSCTs = true;
         break;
       case VerifiedSCT::Status::InvalidSignature:
       case VerifiedSCT::Status::InvalidTimestamp:
         hasInvalidSCTs = true;
         break;
       default:
         MOZ_ASSERT_UNREACHABLE("Unexpected VerifiedSCT::Status type");
--- a/security/manager/tools/getCTKnownLogs.py
+++ b/security/manager/tools/getCTKnownLogs.py
@@ -1,23 +1,29 @@
 #!/usr/bin/env python
 # 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/.
 
 """
 Parses a JSON file listing the known Certificate Transparency logs
-(as downloaded from https://www.certificate-transparency.org/known-logs)
-and generates a C++ header file to be included in Firefox.
+(log_list.json) and generates a C++ header file to be included in Firefox.
+
+The current log_list.json file available under security/manager/tools
+was originally downloaded from
+https://www.certificate-transparency.org/known-logs
+and edited to include the disqualification time for the disqualified logs using
+https://cs.chromium.org/chromium/src/net/cert/ct_known_logs_static-inc.h
 """
 
 from __future__ import print_function
 from string import Template
 import argparse
 import base64
+import datetime
 import json
 import os.path
 import sys
 import textwrap
 import urllib2
 
 
 OUTPUT_TEMPLATE = """\
@@ -27,72 +33,149 @@ OUTPUT_TEMPLATE = """\
  * 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/. */
 
 /* This file was automatically generated by $prog. */
 
 #ifndef $include_guard
 #define $include_guard
 
+#include "CTLog.h"
+
 #include <stddef.h>
 
-struct CTLogInfo {
-  const char* const logName;
-  const char* const logUrl;
-  const char* const logKey;
-  const size_t logKeyLength;
+struct CTLogInfo
+{
+  const char* const name;
+  // Index within kCTLogOperatorList.
+  const mozilla::ct::CTLogStatus status;
+  // 0 for qualified logs, disqualification time for disqualified logs
+  // (in milliseconds, measured since the epoch, ignoring leap seconds).
+  const uint64_t disqualificationTime;
+  const size_t operatorIndex;
+  const char* const key;
+  const size_t keyLength;
+};
+
+struct CTLogOperatorInfo
+{
+  const char* const name;
+  const mozilla::ct::CTLogOperatorId id;
 };
 
 const CTLogInfo kCTLogList[] = {
 $logs
 };
 
+const CTLogOperatorInfo kCTLogOperatorList[] = {
+$operators
+};
+
 #endif // $include_guard
 """
 
 
+def get_disqualification_time(time_str):
+    """
+    Convert a time string such as "2017-01-01T00:00:00Z" to an integer
+    representing milliseconds since the epoch.
+    Timezones in the string are not supported and will result in an exception.
+    """
+    t = datetime.datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%SZ")
+    epoch = datetime.datetime.utcfromtimestamp(0)
+    seconds_since_epoch = (t - epoch).total_seconds()
+    return int(seconds_since_epoch * 1000)
+
+
 def get_hex_lines(blob, width):
     """ Convert a binary string to a multiline text of C escape sequences. """
     text = "".join(["\\x{:02x}".format(ord(c)) for c in blob])
     # When escaped, a single byte takes 4 chars (e.g. "\x00").
     # Make sure we don't break an escaped byte between the lines.
     return textwrap.wrap(text, width - width % 4)
 
 
+def get_operator_and_index(json_data, operator_id):
+    """ Return operator's entry from the JSON along with its array index. """
+    matches = [(operator, index) for (index, operator) in enumerate(
+        json_data["operators"]) if operator["id"] == operator_id]
+    assert len(matches) != 0, "No operators with id {0} defined.".format(
+        operator_id)
+    assert len(matches) == 1, "Found multiple operators with id {0}.".format(
+        operator_id)
+    return matches[0]
+
+
 def get_log_info_structs(json_data):
     """ Return array of CTLogInfo initializers for the known logs. """
     tmpl = Template(textwrap.dedent("""\
           { $description,
-            $url,
+            $status,
+            $disqualification_time, // $disqualification_time_comment
+            $operator_index, // $operator_comment
         $indented_log_key,
             $log_key_len }"""))
     initializers = []
     for log in json_data["logs"]:
         log_key = base64.decodestring(log["key"])
+        # "operated_by" is a list, we assume here it always contains one item.
+        operated_by = log["operated_by"]
+        assert len(operated_by) == 1, "operated_by must contain one item."
+        operator, operator_index = get_operator_and_index(json_data,
+                                                          operated_by[0])
+        if "disqualification_time" in log:
+            status = "mozilla::ct::CTLogStatus::Disqualified"
+            disqualification_time = get_disqualification_time(
+                log["disqualification_time"])
+            disqualification_time_comment = 'Date.parse("{0}")'.format(
+                log["disqualification_time"])
+        else:
+            status = "mozilla::ct::CTLogStatus::Included"
+            disqualification_time = 0
+            disqualification_time_comment = "no disqualification time"
         initializers.append(tmpl.substitute(
             # Use json.dumps for C-escaping strings.
             # Not perfect but close enough.
             description=json.dumps(log["description"]),
-            url=json.dumps("https://{0}/".format(log["url"])),
+            operator_index=operator_index,
+            operator_comment="operated by {0}".
+            # The comment must not contain "/".
+            format(operator["name"]).replace("/", "|"),
+            status=status,
+            disqualification_time=disqualification_time,
+            disqualification_time_comment=disqualification_time_comment,
             # Maximum line width is 80.
             indented_log_key="\n".
             join(['    "{0}"'.format(l) for l in get_hex_lines(log_key, 74)]),
             log_key_len=len(log_key)))
     return initializers
 
 
+def get_log_operator_structs(json_data):
+    """ Return array of CTLogOperatorInfo initializers. """
+    tmpl = Template("  { $name, $id }")
+    initializers = []
+    for operator in json_data["operators"]:
+        initializers.append(tmpl.substitute(
+            name=json.dumps(operator["name"]),
+            id=operator["id"]))
+    return initializers
+
+
 def generate_cpp_header_file(json_data, out_file):
     """ Generate the C++ header file for the known logs. """
     filename = os.path.basename(out_file.name)
     include_guard = filename.replace(".", "_").replace("/", "_")
     log_info_initializers = get_log_info_structs(json_data)
+    operator_info_initializers = get_log_operator_structs(json_data)
     out_file.write(Template(OUTPUT_TEMPLATE).substitute(
         prog=os.path.basename(sys.argv[0]),
         include_guard=include_guard,
-        logs=",\n".join(log_info_initializers)))
+        logs=",\n".join(log_info_initializers),
+        operators=",\n".join(operator_info_initializers)))
 
 
 def run(args):
     """
     Load the input JSON file and generate the C++ header according to the
     command line arguments.
     """
     if args.file:
@@ -121,26 +204,23 @@ def parse_arguments_and_run():
     """ Parse the command line arguments and run the program. """
     arg_parser = argparse.ArgumentParser(
         description="Parses a JSON file listing the known "
         "Certificate Transparency logs and generates "
         "a C++ header file to be included in Firefox.",
         epilog="Example: python %s --url" % os.path.basename(sys.argv[0]))
 
     source_group = arg_parser.add_mutually_exclusive_group(required=True)
-    source_group.add_argument("--file",
-                              help="Read the known CT logs JSON file from the "
-                              "specified location on the filesystem.")
-    source_group.add_argument("--url", nargs="?",
-                              const="https://www.certificate-transparency.org/"
-                              "known-logs/log_list.json",
+    source_group.add_argument("--file", nargs="?",
+                              const="log_list.json",
+                              help="Read the known CT logs JSON data from the "
+                              "specified local file (%(const)s by default).")
+    source_group.add_argument("--url",
                               help="Download the known CT logs JSON file "
-                              "from the specified URL. "
-                              "If no URL is given, download the file "
-                              "from %(const)s.")
+                              "from the specified URL.")
 
     arg_parser.add_argument("--out",
                             default="../../certverifier/CTKnownLogs.h",
                             help="Path and filename of the header file "
                             "to be generated. Defaults to %(default)s")
 
     run(arg_parser.parse_args())
 
new file mode 100644
--- /dev/null
+++ b/security/manager/tools/log_list.json
@@ -0,0 +1,128 @@
+{
+  "operators": [
+  {
+    "name": "Google",
+    "id": 0
+  },
+  {
+    "name": "DigiCert",
+    "id": 1
+  },
+  {
+    "name": "Certly",
+    "id": 2
+  },
+  {
+    "name": "Izenpe",
+    "id": 3
+  },
+  {
+    "name": "Symantec",
+    "id": 4
+  },
+  {
+    "name": "Venafi",
+    "id": 5
+  },
+  {
+    "name": "CNNIC",
+    "id": 7
+  },
+  {
+    "name": "WoSign",
+    "id": 8
+  },
+  {
+    "name": "StartCom",
+    "id": 9
+  }
+  ],
+  "logs": [
+    {
+      "description": "Google 'Pilot' log",
+      "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEfahLEimAoz2t01p3uMziiLOl/fHTDM0YDOhBRuiBARsV4UvxG2LdNgoIGLrtCzWE0J5APC2em4JlvR8EEEFMoA==",
+      "url": "ct.googleapis.com/pilot",
+      "maximum_merge_delay": 86400,
+      "operated_by": [0]
+    },
+    {
+      "description": "Google 'Aviator' log",
+      "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE1/TMabLkDpCjiupacAlP7xNi0I1JYP8bQFAHDG1xhtolSY1l4QgNRzRrvSe8liE+NPWHdjGxfx3JhTsN9x8/6Q==",
+      "url": "ct.googleapis.com/aviator",
+      "maximum_merge_delay": 86400,
+      "operated_by": [0]
+    },
+    {
+      "description": "DigiCert Log Server",
+      "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEAkbFvhu7gkAW6MHSrBlpE1n4+HCFRkC5OLAjgqhkTH+/uzSfSl8ois8ZxAD2NgaTZe1M9akhYlrYkes4JECs6A==",
+      "url": "ct1.digicert-ct.com/log",
+      "maximum_merge_delay": 86400,
+      "operated_by": [1]
+    },
+    {
+      "description": "Google 'Rocketeer' log",
+      "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIFsYyDzBi7MxCAC/oJBXK7dHjG+1aLCOkHjpoHPqTyghLpzA9BYbqvnV16mAw04vUjyYASVGJCUoI3ctBcJAeg==",
+      "url": "ct.googleapis.com/rocketeer",
+      "maximum_merge_delay": 86400,
+      "operated_by": [0]
+    },
+    {
+      "description": "Certly.IO log",
+      "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAECyPLhWKYYUgEc+tUXfPQB4wtGS2MNvXrjwFCCnyYJifBtd2Sk7Cu+Js9DNhMTh35FftHaHu6ZrclnNBKwmbbSA==",
+      "url": "log.certly.io",
+      "maximum_merge_delay": 86400,
+      "operated_by": [2],
+      "disqualification_time": "2016-04-15T00:00:00Z"
+    },
+    {
+      "description": "Izenpe log",
+      "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEJ2Q5DC3cUBj4IQCiDu0s6j51up+TZAkAEcQRF6tczw90rLWXkJMAW7jr9yc92bIKgV8vDXU4lDeZHvYHduDuvg==",
+      "url": "ct.izenpe.com",
+      "maximum_merge_delay": 86400,
+      "operated_by": [3],
+      "disqualification_time": "2016-05-30T00:00:00Z"
+    },
+    {
+      "description": "Symantec log",
+      "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEluqsHEYMG1XcDfy1lCdGV0JwOmkY4r87xNuroPS2bMBTP01CEDPwWJePa75y9CrsHEKqAy8afig1dpkIPSEUhg==",
+      "url": "ct.ws.symantec.com",
+      "maximum_merge_delay": 86400,
+      "operated_by": [4]
+    },
+    {
+      "description": "Venafi log",
+      "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAolpIHxdSlTXLo1s6H1OCdpSj/4DyHDc8wLG9wVmLqy1lk9fz4ATVmm+/1iN2Nk8jmctUKK2MFUtlWXZBSpym97M7frGlSaQXUWyA3CqQUEuIJOmlEjKTBEiQAvpfDjCHjlV2Be4qTM6jamkJbiWtgnYPhJL6ONaGTiSPm7Byy57iaz/hbckldSOIoRhYBiMzeNoA0DiRZ9KmfSeXZ1rB8y8X5urSW+iBzf2SaOfzBvDpcoTuAaWx2DPazoOl28fP1hZ+kHUYvxbcMjttjauCFx+JII0dmuZNIwjfeG/GBb9frpSX219k1O4Wi6OEbHEr8at/XQ0y7gTikOxBn/s5wQIDAQAB",
+      "url": "ctlog.api.venafi.com",
+      "maximum_merge_delay": 86400,
+      "operated_by": [5]
+    },
+    {
+      "description": "Symantec 'Vega' log",
+      "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE6pWeAv/u8TNtS4e8zf0ZF2L/lNPQWQc/Ai0ckP7IRzA78d0NuBEMXR2G3avTK0Zm+25ltzv9WWis36b4ztIYTQ==",
+      "url": "vega.ws.symantec.com",
+      "maximum_merge_delay": 86400,
+      "operated_by": [4]
+    },
+    {
+      "description": "CNNIC CT log",
+      "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7UIYZopMgTTJWPp2IXhhuAf1l6a9zM7gBvntj5fLaFm9pVKhKYhVnno94XuXeN8EsDgiSIJIj66FpUGvai5samyetZhLocRuXhAiXXbDNyQ4KR51tVebtEq2zT0mT9liTtGwiksFQccyUsaVPhsHq9gJ2IKZdWauVA2Fm5x9h8B9xKn/L/2IaMpkIYtd967TNTP/dLPgixN1PLCLaypvurDGSVDsuWabA3FHKWL9z8wr7kBkbdpEhLlg2H+NAC+9nGKx+tQkuhZ/hWR65aX+CNUPy2OB9/u2rNPyDydb988LENXoUcMkQT0dU3aiYGkFAY0uZjD2vH97TM20xYtNQIDAQAB",
+      "url": "ctserver.cnnic.cn",
+      "maximum_merge_delay": 86400,
+      "operated_by": [7]
+    },
+    {
+      "description": "WoSign log",
+      "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEzBGIey1my66PTTBmJxklIpMhRrQvAdPG+SvVyLpzmwai8IoCnNBrRhgwhbrpJIsO0VtwKAx+8TpFf1rzgkJgMQ==",
+      "url": "ctlog.wosign.com",
+      "maximum_merge_delay": 86400,
+      "operated_by": [8]
+    },
+    {
+      "description": "StartCom log",
+      "key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAESPNZ8/YFGNPbsu1Gfs/IEbVXsajWTOaft0oaFIZDqUiwy1o/PErK38SCFFWa+PeOQFXc9NKv6nV0+05/YIYuUQ==",
+      "url": "ct.startssl.com",
+      "maximum_merge_delay": 86400,
+      "operated_by": [9]
+    }
+  ]
+}
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -8558,17 +8558,17 @@
   },
   "SSL_SCTS_VERIFICATION_STATUS": {
     "alert_emails": ["seceng-telemetry@mozilla.com"],
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values": 10,
     "bug_numbers": [1293231],
     "releaseChannelCollection": "opt-out",
-    "description": "Verification status of Signed Certificate Timestamps received (0=Decoding error, 1=SCT verified, 2=SCT from unknown log, 3=Invalid SCT signature, 4=SCT timestamp is in the future)"
+    "description": "Verification status of Signed Certificate Timestamps received (0=Decoding error, 1=Valid SCT, 2=SCT from unknown log, 3=Invalid SCT signature, 4=SCT timestamp is in the future, 5=Valid SCT from a disqualified log)"
   },
   "SSL_SERVER_AUTH_EKU": {
     "alert_emails": ["seceng-telemetry@mozilla.com"],
     "expires_in_version": "never",
     "kind": "enumerated",
     "n_values": 10,
     "description": "Presence of of the Server Authenticaton EKU in accepted SSL server certificates (0=No EKU, 1=EKU present and has id_kp_serverAuth, 2=EKU present and has id_kp_serverAuth as well as some other EKU, 3=EKU present but does not contain id_kp_serverAuth)"
   },