summaryrefslogtreecommitdiffstats
path: root/security/ct
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-21 11:44:51 +0000
commit9e3c08db40b8916968b9f30096c7be3f00ce9647 (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /security/ct
parentInitial commit. (diff)
downloadthunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.tar.xz
thunderbird-9e3c08db40b8916968b9f30096c7be3f00ce9647.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'security/ct')
-rw-r--r--security/ct/BTTypes.h64
-rw-r--r--security/ct/BTVerifier.cpp322
-rw-r--r--security/ct/BTVerifier.h46
-rw-r--r--security/ct/Buffer.cpp20
-rw-r--r--security/ct/Buffer.h21
-rw-r--r--security/ct/CTDiversityPolicy.cpp39
-rw-r--r--security/ct/CTDiversityPolicy.h43
-rw-r--r--security/ct/CTKnownLogs.h341
-rw-r--r--security/ct/CTLog.h38
-rw-r--r--security/ct/CTLogVerifier.cpp305
-rw-r--r--security/ct/CTLogVerifier.h88
-rw-r--r--security/ct/CTObjectsExtractor.cpp378
-rw-r--r--security/ct/CTObjectsExtractor.h45
-rw-r--r--security/ct/CTPolicyEnforcer.cpp277
-rw-r--r--security/ct/CTPolicyEnforcer.h65
-rw-r--r--security/ct/CTSerialization.cpp396
-rw-r--r--security/ct/CTSerialization.h65
-rw-r--r--security/ct/CTUtils.h111
-rw-r--r--security/ct/CTVerifyResult.cpp26
-rw-r--r--security/ct/CTVerifyResult.h84
-rw-r--r--security/ct/MultiLogCTVerifier.cpp194
-rw-r--r--security/ct/MultiLogCTVerifier.h89
-rw-r--r--security/ct/SignedCertificateTimestamp.cpp27
-rw-r--r--security/ct/SignedCertificateTimestamp.h92
-rw-r--r--security/ct/moz.build53
-rw-r--r--security/ct/tests/gtest/BTSerializationTest.cpp140
-rw-r--r--security/ct/tests/gtest/BTSignedTreeHeadTest.cpp266
-rw-r--r--security/ct/tests/gtest/BTVerificationTest.cpp257
-rw-r--r--security/ct/tests/gtest/CTDiversityPolicyTest.cpp15
-rw-r--r--security/ct/tests/gtest/CTLogVerifierTest.cpp118
-rw-r--r--security/ct/tests/gtest/CTObjectsExtractorTest.cpp82
-rw-r--r--security/ct/tests/gtest/CTPolicyEnforcerTest.cpp387
-rw-r--r--security/ct/tests/gtest/CTSerializationTest.cpp216
-rw-r--r--security/ct/tests/gtest/CTTestUtils.cpp820
-rw-r--r--security/ct/tests/gtest/CTTestUtils.h137
-rw-r--r--security/ct/tests/gtest/MultiLogCTVerifierTest.cpp254
-rwxr-xr-xsecurity/ct/tests/gtest/createSTHTestData.py133
-rw-r--r--security/ct/tests/gtest/ec-signer-rsa-spki-sth.inc.tbs8
-rw-r--r--security/ct/tests/gtest/missing-extensions-sth.inc.tbs8
-rw-r--r--security/ct/tests/gtest/missing-log-id-sth.inc.tbs8
-rw-r--r--security/ct/tests/gtest/missing-root-hash-sth.inc.tbs7
-rw-r--r--security/ct/tests/gtest/missing-timestamp-sth.inc.tbs8
-rw-r--r--security/ct/tests/gtest/missing-tree-size-sth.inc.tbs8
-rw-r--r--security/ct/tests/gtest/moz.build56
-rw-r--r--security/ct/tests/gtest/rsa-signer-ec-spki-sth.inc.tbs8
-rw-r--r--security/ct/tests/gtest/rsa-signer-rsa-spki-sth.inc.tbs8
-rw-r--r--security/ct/tests/gtest/signature-covers-log-id-sth.inc.tbs8
-rw-r--r--security/ct/tests/gtest/truncated-extension-sth.inc.tbs11
-rw-r--r--security/ct/tests/gtest/truncated-log-id-sth.inc.tbs8
-rw-r--r--security/ct/tests/gtest/truncated-root-hash-sth.inc.tbs8
-rw-r--r--security/ct/tests/gtest/truncated-timestamp-sth.inc.tbs8
-rw-r--r--security/ct/tests/gtest/truncated-tree-size-sth.inc.tbs8
-rw-r--r--security/ct/tests/gtest/valid-secp521r1-sha512-sth.inc.tbs10
-rw-r--r--security/ct/tests/gtest/valid-sth.inc.tbs8
-rw-r--r--security/ct/tests/gtest/valid-with-extension-sth.inc.tbs11
-rw-r--r--security/ct/tests/gtest/wrong-signing-key-sth.inc.tbs8
-rw-r--r--security/ct/tests/gtest/wrong-spki-sth.inc.tbs8
57 files changed, 6269 insertions, 0 deletions
diff --git a/security/ct/BTTypes.h b/security/ct/BTTypes.h
new file mode 100644
index 0000000000..aa16072150
--- /dev/null
+++ b/security/ct/BTTypes.h
@@ -0,0 +1,64 @@
+/* -*- 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 BTTypes_h
+#define BTTypes_h
+
+#include <vector>
+
+#include "Buffer.h"
+
+namespace mozilla {
+namespace ct {
+
+// Represents a Merkle inclusion proof for purposes of serialization,
+// deserialization, and verification of the proof. The format for inclusion
+// proofs in RFC 6962-bis is as follows:
+//
+// opaque LogID<2..127>;
+// opaque NodeHash<32..2^8-1>;
+//
+// struct {
+// LogID log_id;
+// uint64 tree_size;
+// uint64 leaf_index;
+// NodeHash inclusion_path<1..2^16-1>;
+// } InclusionProofDataV2;
+
+struct InclusionProofDataV2 {
+ Buffer logId;
+ uint64_t treeSize;
+ uint64_t leafIndex;
+ std::vector<Buffer> inclusionPath;
+};
+
+// Represents a Signed Tree Head as per RFC 6962-bis. All extensions are
+// ignored. The signature field covers the data in the tree_head field.
+//
+// struct {
+// LogID log_id;
+// TreeHeadDataV2 tree_head;
+// opaque signature<0..2^16-1>;
+// } SignedTreeHeadDataV2;
+//
+// struct {
+// uint64 timestamp;
+// uint64 tree_size;
+// NodeHash root_hash;
+// Extension sth_extensions<0..2^16-1>;
+// } TreeHeadDataV2;
+
+struct SignedTreeHeadDataV2 {
+ Buffer logId;
+ uint64_t timestamp;
+ uint64_t treeSize;
+ Buffer rootHash;
+};
+
+} // namespace ct
+} // namespace mozilla
+
+#endif // BTTypes_h
diff --git a/security/ct/BTVerifier.cpp b/security/ct/BTVerifier.cpp
new file mode 100644
index 0000000000..02281fcffd
--- /dev/null
+++ b/security/ct/BTVerifier.cpp
@@ -0,0 +1,322 @@
+/* -*- 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 "BTVerifier.h"
+
+#include <stdint.h>
+
+#include "CTUtils.h"
+#include "SignedCertificateTimestamp.h"
+#include "hasht.h"
+#include "mozpkix/pkixnss.h"
+#include "mozpkix/pkixutil.h"
+
+namespace mozilla {
+namespace ct {
+
+using namespace mozilla::pkix;
+
+typedef mozilla::pkix::Result Result;
+
+// Common prefix lengths
+static const size_t kLogIdPrefixLengthBytes = 1;
+static const size_t kBTTreeSizeLength = 8;
+static const size_t kNodeHashPrefixLengthBytes = 1;
+
+// Members of a SignedTreeHeadDataV2 struct
+static const size_t kSTHTimestampLength = 8;
+static const size_t kSTHExtensionsLengthBytes = 2;
+static const size_t kSTHSignatureLengthBytes = 2;
+
+// Members of a Inclusion Proof struct
+static const size_t kLeafIndexLength = 8;
+static const size_t kInclusionPathLengthBytes = 2;
+
+static Result GetDigestAlgorithmLengthAndIdentifier(
+ DigestAlgorithm digestAlgorithm,
+ /* out */ size_t& digestAlgorithmLength,
+ /* out */ SECOidTag& digestAlgorithmId) {
+ switch (digestAlgorithm) {
+ case DigestAlgorithm::sha512:
+ digestAlgorithmLength = SHA512_LENGTH;
+ digestAlgorithmId = SEC_OID_SHA512;
+ return Success;
+ case DigestAlgorithm::sha256:
+ digestAlgorithmLength = SHA256_LENGTH;
+ digestAlgorithmId = SEC_OID_SHA256;
+ return Success;
+ default:
+ return pkix::Result::FATAL_ERROR_INVALID_ARGS;
+ }
+}
+
+Result DecodeAndVerifySignedTreeHead(
+ Input signerSubjectPublicKeyInfo, DigestAlgorithm digestAlgorithm,
+ der::PublicKeyAlgorithm publicKeyAlgorithm, Input signedTreeHeadInput,
+ /* out */ SignedTreeHeadDataV2& signedTreeHead) {
+ SignedTreeHeadDataV2 result;
+ Reader reader(signedTreeHeadInput);
+
+ Input logId;
+ Result rv = ReadVariableBytes<kLogIdPrefixLengthBytes>(reader, logId);
+ if (rv != Success) {
+ return rv;
+ }
+ InputToBuffer(logId, result.logId);
+
+ // This is the beginning of the data covered by the signature.
+ Reader::Mark signedDataMark = reader.GetMark();
+
+ rv = ReadUint<kSTHTimestampLength>(reader, result.timestamp);
+ if (rv != Success) {
+ return rv;
+ }
+
+ rv = ReadUint<kBTTreeSizeLength>(reader, result.treeSize);
+ if (rv != Success) {
+ return rv;
+ }
+
+ Input hash;
+ rv = ReadVariableBytes<kNodeHashPrefixLengthBytes>(reader, hash);
+ if (rv != Success) {
+ return rv;
+ }
+ InputToBuffer(hash, result.rootHash);
+
+ // We ignore any extensions, but we have to read them.
+ Input extensionsInput;
+ rv = ReadVariableBytes<kSTHExtensionsLengthBytes>(reader, extensionsInput);
+ if (rv != Success) {
+ return rv;
+ }
+
+ Input signedDataInput;
+ rv = reader.GetInput(signedDataMark, signedDataInput);
+ if (rv != Success) {
+ return rv;
+ }
+
+ Input signatureInput;
+ rv = ReadVariableBytes<kSTHSignatureLengthBytes>(reader, signatureInput);
+ if (rv != Success) {
+ return rv;
+ }
+
+ switch (publicKeyAlgorithm) {
+ case der::PublicKeyAlgorithm::ECDSA:
+ rv = VerifyECDSASignedDataNSS(signedDataInput, digestAlgorithm,
+ signatureInput, signerSubjectPublicKeyInfo,
+ nullptr);
+ break;
+ case der::PublicKeyAlgorithm::RSA_PKCS1:
+ default:
+ return Result::FATAL_ERROR_INVALID_ARGS;
+ }
+ if (rv != Success) {
+ return rv;
+ }
+
+ if (!reader.AtEnd()) {
+ return pkix::Result::ERROR_BAD_DER;
+ }
+
+ signedTreeHead = std::move(result);
+ return Success;
+}
+
+Result DecodeInclusionProof(Input input, InclusionProofDataV2& output) {
+ InclusionProofDataV2 result;
+ Reader reader(input);
+
+ Input logId;
+ Result rv = ReadVariableBytes<kLogIdPrefixLengthBytes>(reader, logId);
+ if (rv != Success) {
+ return rv;
+ }
+
+ rv = ReadUint<kBTTreeSizeLength>(reader, result.treeSize);
+ if (rv != Success) {
+ return rv;
+ }
+
+ if (result.treeSize < 1) {
+ return pkix::Result::ERROR_BAD_DER;
+ }
+
+ rv = ReadUint<kLeafIndexLength>(reader, result.leafIndex);
+ if (rv != Success) {
+ return rv;
+ }
+
+ if (result.leafIndex >= result.treeSize) {
+ return pkix::Result::ERROR_BAD_DER;
+ }
+
+ Input pathInput;
+ rv = ReadVariableBytes<kInclusionPathLengthBytes>(reader, pathInput);
+ if (rv != Success) {
+ return rv;
+ }
+
+ if (pathInput.GetLength() < 1) {
+ return pkix::Result::ERROR_BAD_DER;
+ }
+
+ Reader pathReader(pathInput);
+ std::vector<Buffer> inclusionPath;
+
+ while (!pathReader.AtEnd()) {
+ Input hash;
+ rv = ReadVariableBytes<kNodeHashPrefixLengthBytes>(pathReader, hash);
+ if (rv != Success) {
+ return rv;
+ }
+
+ Buffer hashBuffer;
+ InputToBuffer(hash, hashBuffer);
+
+ inclusionPath.push_back(std::move(hashBuffer));
+ }
+
+ if (!reader.AtEnd()) {
+ return pkix::Result::ERROR_BAD_DER;
+ }
+
+ InputToBuffer(logId, result.logId);
+
+ result.inclusionPath = std::move(inclusionPath);
+
+ output = std::move(result);
+ return Success;
+}
+
+static Result CommonFinishDigest(UniquePK11Context& context,
+ size_t digestAlgorithmLength,
+ /* out */ Buffer& outputBuffer) {
+ uint32_t outLen = 0;
+ outputBuffer.assign(digestAlgorithmLength, 0);
+ if (PK11_DigestFinal(context.get(), outputBuffer.data(), &outLen,
+ digestAlgorithmLength) != SECSuccess) {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+ if (outLen != digestAlgorithmLength) {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+ return Success;
+}
+
+static Result LeafHash(Input leafEntry, size_t digestAlgorithmLength,
+ SECOidTag digestAlgorithmId,
+ /* out */ Buffer& calculatedHash) {
+ UniquePK11Context context(PK11_CreateDigestContext(digestAlgorithmId));
+ if (!context) {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+ const unsigned char zero = 0;
+ if (PK11_DigestOp(context.get(), &zero, 1u) != SECSuccess) {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+ SECItem leafEntryItem = UnsafeMapInputToSECItem(leafEntry);
+ if (PK11_DigestOp(context.get(), leafEntryItem.data, leafEntryItem.len) !=
+ SECSuccess) {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+ return CommonFinishDigest(context, digestAlgorithmLength, calculatedHash);
+}
+
+static Result NodeHash(const Buffer& left, const Buffer& right,
+ size_t digestAlgorithmLength,
+ SECOidTag digestAlgorithmId,
+ /* out */ Buffer& calculatedHash) {
+ UniquePK11Context context(PK11_CreateDigestContext(digestAlgorithmId));
+ if (!context) {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+ const unsigned char one = 1;
+ if (PK11_DigestOp(context.get(), &one, 1u) != SECSuccess) {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+ if (PK11_DigestOp(context.get(), left.data(), left.size()) != SECSuccess) {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+ if (PK11_DigestOp(context.get(), right.data(), right.size()) != SECSuccess) {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+ return CommonFinishDigest(context, digestAlgorithmLength, calculatedHash);
+}
+
+// This algorithm is specified by:
+// https://tools.ietf.org/html/draft-ietf-trans-rfc6962-bis-28#section-2.1.3.2
+Result VerifyInclusionProof(const InclusionProofDataV2& proof, Input leafEntry,
+ Input expectedRootHash,
+ DigestAlgorithm digestAlgorithm) {
+ if (proof.treeSize == 0) {
+ return pkix::Result::ERROR_BAD_SIGNATURE;
+ }
+ size_t digestAlgorithmLength;
+ SECOidTag digestAlgorithmId;
+ Result rv = GetDigestAlgorithmLengthAndIdentifier(
+ digestAlgorithm, digestAlgorithmLength, digestAlgorithmId);
+ if (rv != Success) {
+ return rv;
+ }
+ if (proof.leafIndex >= proof.treeSize) {
+ return pkix::Result::ERROR_BAD_SIGNATURE;
+ }
+ if (expectedRootHash.GetLength() != digestAlgorithmLength) {
+ return pkix::Result::ERROR_BAD_SIGNATURE;
+ }
+ uint64_t leafIndex = proof.leafIndex;
+ uint64_t lastNodeIndex = proof.treeSize - 1;
+ Buffer calculatedHash;
+ rv = LeafHash(leafEntry, digestAlgorithmLength, digestAlgorithmId,
+ calculatedHash);
+ if (rv != Success) {
+ return rv;
+ }
+ for (const auto& hash : proof.inclusionPath) {
+ if (lastNodeIndex == 0) {
+ return pkix::Result::ERROR_BAD_SIGNATURE;
+ }
+ if (leafIndex % 2 == 1 || leafIndex == lastNodeIndex) {
+ rv = NodeHash(hash, calculatedHash, digestAlgorithmLength,
+ digestAlgorithmId, calculatedHash);
+ if (rv != Success) {
+ return rv;
+ }
+ if (leafIndex % 2 == 0) {
+ while (leafIndex % 2 == 0 && lastNodeIndex > 0) {
+ leafIndex >>= 1;
+ lastNodeIndex >>= 1;
+ }
+ }
+ } else {
+ rv = NodeHash(calculatedHash, hash, digestAlgorithmLength,
+ digestAlgorithmId, calculatedHash);
+ if (rv != Success) {
+ return rv;
+ }
+ }
+ leafIndex >>= 1;
+ lastNodeIndex >>= 1;
+ }
+ if (lastNodeIndex != 0) {
+ return pkix::Result::ERROR_BAD_SIGNATURE;
+ }
+ assert(calculatedHash.size() == digestAlgorithmLength);
+ if (calculatedHash.size() != digestAlgorithmLength) {
+ return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+ if (memcmp(calculatedHash.data(), expectedRootHash.UnsafeGetData(),
+ digestAlgorithmLength) != 0) {
+ return pkix::Result::ERROR_BAD_SIGNATURE;
+ }
+ return Success;
+}
+
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/BTVerifier.h b/security/ct/BTVerifier.h
new file mode 100644
index 0000000000..543493797c
--- /dev/null
+++ b/security/ct/BTVerifier.h
@@ -0,0 +1,46 @@
+/* -*- 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 BTVerifier_h
+#define BTVerifier_h
+
+#include "BTTypes.h"
+#include "mozpkix/Input.h"
+#include "mozpkix/Result.h"
+#include "mozpkix/pkixder.h"
+#include "mozpkix/pkixtypes.h"
+
+namespace mozilla {
+namespace ct {
+
+// Given an encoded subject public key info, a digest algorithm, a public key
+// algorithm, and an encoded signed tree head (SignedTreeHeadV2 as defined in
+// RFC 6962-bis), decodes and verifies the STH. Currently only ECDSA keys are
+// supported. SHA-256 and SHA-512 are supported, but according to the
+// specification only SHA-256 should be supported at this time.
+pkix::Result DecodeAndVerifySignedTreeHead(
+ pkix::Input signerSubjectPublicKeyInfo,
+ pkix::DigestAlgorithm digestAlgorithm,
+ pkix::der::PublicKeyAlgorithm publicKeyAlg, pkix::Input signedTreeHeadInput,
+ SignedTreeHeadDataV2& signedTreeHead);
+
+// Decodes an Inclusion Proof (InclusionProofDataV2 as defined in RFC
+// 6962-bis). This consumes the entirety of the input.
+pkix::Result DecodeInclusionProof(pkix::Input input,
+ InclusionProofDataV2& output);
+// Given a decoded Inclusion Proof (InclusionProofDataV2 as per RFC 6962-bis),
+// the corresponding leaf data entry, and the expected root hash, returns
+// Success if the proof verifies correctly.
+// (https://tools.ietf.org/html/draft-ietf-trans-rfc6962-bis-28#section-2.1.3.2)
+pkix::Result VerifyInclusionProof(const InclusionProofDataV2& proof,
+ pkix::Input leafEntry,
+ pkix::Input expectedRootHash,
+ pkix::DigestAlgorithm digestAlgorithm);
+
+} // namespace ct
+} // namespace mozilla
+
+#endif // BTVerifier_h
diff --git a/security/ct/Buffer.cpp b/security/ct/Buffer.cpp
new file mode 100644
index 0000000000..09f39e6212
--- /dev/null
+++ b/security/ct/Buffer.cpp
@@ -0,0 +1,20 @@
+/* -*- 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 "Buffer.h"
+
+#include <string.h>
+
+namespace mozilla {
+
+bool operator==(const ct::Buffer& a, const ct::Buffer& b) {
+ return (a.empty() && b.empty()) ||
+ (a.size() == b.size() && memcmp(a.data(), b.data(), a.size()) == 0);
+}
+
+bool operator!=(const ct::Buffer& a, const ct::Buffer& b) { return !(a == b); }
+
+} // namespace mozilla
diff --git a/security/ct/Buffer.h b/security/ct/Buffer.h
new file mode 100644
index 0000000000..20d5a2e25a
--- /dev/null
+++ b/security/ct/Buffer.h
@@ -0,0 +1,21 @@
+/* -*- 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 Buffer_h
+#define Buffer_h
+
+#include <vector>
+#include <cstdint>
+
+namespace mozilla {
+namespace ct {
+
+typedef std::vector<uint8_t> Buffer;
+
+}
+} // namespace mozilla
+
+#endif // Buffer_h
diff --git a/security/ct/CTDiversityPolicy.cpp b/security/ct/CTDiversityPolicy.cpp
new file mode 100644
index 0000000000..dbb80c1321
--- /dev/null
+++ b/security/ct/CTDiversityPolicy.cpp
@@ -0,0 +1,39 @@
+/* -*- 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 "CTDiversityPolicy.h"
+
+namespace mozilla {
+namespace ct {
+
+typedef pkix::Result Result;
+
+void GetCTLogOperatorsFromVerifiedSCTList(const VerifiedSCTList& list,
+ CTLogOperatorList& operators) {
+ operators.clear();
+ for (const VerifiedSCT& verifiedSct : list) {
+ CTLogOperatorId sctLogOperatorId = verifiedSct.logOperatorId;
+ bool alreadyAdded = false;
+ for (CTLogOperatorId id : operators) {
+ if (id == sctLogOperatorId) {
+ alreadyAdded = true;
+ break;
+ }
+ }
+ if (!alreadyAdded) {
+ operators.push_back(sctLogOperatorId);
+ }
+ }
+}
+
+Result CTDiversityPolicy::GetDependentOperators(
+ const nsTArray<nsTArray<uint8_t>>& builtChain,
+ const CTLogOperatorList& operators, CTLogOperatorList& dependentOperators) {
+ return pkix::Success;
+}
+
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/CTDiversityPolicy.h b/security/ct/CTDiversityPolicy.h
new file mode 100644
index 0000000000..783e5f6db1
--- /dev/null
+++ b/security/ct/CTDiversityPolicy.h
@@ -0,0 +1,43 @@
+/* -*- 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 CTDiversityPolicy_h
+#define CTDiversityPolicy_h
+
+#include "CTLog.h"
+#include "CTVerifyResult.h"
+#include "certt.h"
+#include "nsTArray.h"
+#include "mozpkix/Result.h"
+
+namespace mozilla {
+namespace ct {
+
+// Retuns the list of unique CT log operator IDs appearing in the provided
+// list of verified SCTs.
+void GetCTLogOperatorsFromVerifiedSCTList(const VerifiedSCTList& list,
+ CTLogOperatorList& operators);
+
+// Helper class used by CTPolicyEnforcer to check the CT log operators
+// diversity requirements of the CT Policy.
+// See CTPolicyEnforcer.h for more details.
+class CTDiversityPolicy {
+ public:
+ // Given a certificate chain and a set of CT log operators,
+ // returns the subset of log operators that are dependent on the CA
+ // issuing the certificate (as defined by the CT Policy).
+ //
+ // NOTE: TBD, PENDING FINALIZATION OF MOZILLA CT POLICY.
+ pkix::Result GetDependentOperators(
+ const nsTArray<nsTArray<uint8_t>>& builtChain,
+ const CTLogOperatorList& operators,
+ CTLogOperatorList& dependentOperators);
+};
+
+} // namespace ct
+} // namespace mozilla
+
+#endif // CTDiversityPolicy_h
diff --git a/security/ct/CTKnownLogs.h b/security/ct/CTKnownLogs.h
new file mode 100644
index 0000000000..d3c04b1b52
--- /dev/null
+++ b/security/ct/CTKnownLogs.h
@@ -0,0 +1,341 @@
+/* -*- 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/. */
+
+/* This file was automatically generated by getCTKnownLogs.py. */
+
+#ifndef CTKnownLogs_h
+#define CTKnownLogs_h
+
+#include "CTLog.h"
+
+#include <stddef.h>
+
+struct CTLogInfo {
+ // See bug 1338873 about making these fields const.
+ const char* name;
+ // Index within kCTLogOperatorList.
+ mozilla::ct::CTLogStatus status;
+ // 0 for qualified logs, disqualification time for disqualified logs
+ // (in milliseconds, measured since the epoch, ignoring leap seconds).
+ uint64_t disqualificationTime;
+ size_t operatorIndex;
+ const char* key;
+ size_t keyLength;
+};
+
+struct CTLogOperatorInfo {
+ // See bug 1338873 about making these fields const.
+ const char* name;
+ mozilla::ct::CTLogOperatorId id;
+};
+
+const CTLogInfo kCTLogList[] = {
+ {"Google 'Argon2023' log", 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\xd0\x90\x8f\x64\x52\x4e\x42\xac\x84"
+ "\xb6\x2e\x4c\xf2\x3d\x77\x00\xb3\x77\x08\x05\x47\xaa\x45\x4c\xe3\x2c\x8e"
+ "\x70\xa5\x82\xbb\x6c\xb2\x7b\x9c\x98\x7a\xa0\xe9\x11\x76\x28\x00\xb2\x20"
+ "\xb4\xcd\xd3\x98\x7b\x4d\x96\x27\xe6\xb7\xee\x22\x6a\xd1\xb0\x2e\x91\x77"
+ "\x78",
+ 91},
+ {"Google 'Argon2024' log", 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\x1d\xb9\x6c\xa9\xcb\x69\x94\xc5\x5c"
+ "\xe6\xb6\xa6\x03\xbb\xd2\xb8\xdc\x54\x43\x17\x28\x99\x0c\x06\x01\x50\x1d"
+ "\x9d\x64\xc0\x59\x46\x2b\xdc\xc8\x03\x1d\x05\xb4\x2d\xa8\x09\xf7\x99\x41"
+ "\xed\x04\xfb\xe5\x57\xba\x26\x04\xf6\x11\x52\xce\x14\x65\x3b\x2f\x76\x2b"
+ "\xc0",
+ 91},
+ {"Google 'Xenon2023' log", 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\x72\x16\x3e\x0b\xef\xef\xce\x3e\x60"
+ "\xdd\x95\xcb\x63\x7a\xb9\xa9\x8d\x4a\x6f\x6c\xdc\x61\x80\xa6\x45\x5e\x2f"
+ "\x83\xac\x94\xf3\x85\x88\xd0\xa5\x74\xd0\x7b\x8e\xff\xc5\xee\x42\xa2\xf0"
+ "\x2d\x93\xe3\xc2\xd0\xb2\x99\xe2\xe1\x42\xe9\xd2\xc6\x00\x27\x69\x74\xae"
+ "\xce",
+ 91},
+ {"Google 'Xenon2024' log", 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\xb9\x60\xe0\x34\x1e\x35\xe4\x65\x00"
+ "\x93\x4f\x90\x09\xbd\x5a\xec\x44\xdd\x8c\x0f\xce\xed\x11\x3e\x2a\x59\x46"
+ "\x9a\x31\xb6\xc7\x99\xf7\xdc\xef\x3d\xcd\x8f\x86\xc2\x35\xa5\x3e\xdc\x29"
+ "\xba\xbb\xf2\x54\xe2\xa8\x0c\x83\x08\x51\x06\xde\x21\x6d\x36\x50\x8e\x38"
+ "\x4d",
+ 91},
+ {"Google 'Icarus' log", 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\x4e\xd2\xbc\xbf\xb3\x08\x0a\xf7\xb9"
+ "\xea\xa4\xc7\x1c\x38\x61\x04\xeb\x95\xe0\x89\x54\x68\x44\xb1\x66\xbc\x82"
+ "\x7e\x4f\x50\x6c\x6f\x5c\xa3\xf0\xaa\x3e\xf4\xec\x80\xf0\xdb\x0a\x9a\x7a"
+ "\xa0\x5b\x72\x00\x7c\x25\x0e\x19\xef\xaf\xb2\x62\x8d\x74\x43\xf4\x26\xf6"
+ "\x14",
+ 91},
+ {"Google 'Pilot' log", 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 'Rocketeer' log", 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},
+ {"Google 'Skydiver' log", 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\x12\x6c\x86\x0e\xf6\x17\xb1\x12\x6c"
+ "\x37\x25\xd2\xad\x87\x3d\x0e\x31\xec\x21\xad\xb1\xcd\xbe\x14\x47\xb6\x71"
+ "\x56\x85\x7a\x9a\xb7\x3d\x89\x90\x7b\xc6\x32\x3a\xf8\xda\xce\x8b\x01\xfe"
+ "\x3f\xfc\x71\x91\x19\x8e\x14\x6e\x89\x7a\x5d\xb4\xab\x7e\xe1\x4e\x1e\x7c"
+ "\xac",
+ 91},
+ {"Cloudflare 'Nimbus2023' Log", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 1, // operated by Cloudflare
+ "\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\x8b\xff\x2d\x92\x18\xcb\x46\x9d\x12"
+ "\x5e\xb9\x59\x75\x3c\xcd\x91\x37\x7a\x1e\xa9\x9c\x99\x78\x83\x27\x3d\xdf"
+ "\x01\xd5\x8b\x80\xe8\x63\x9a\xfe\x26\xa2\x1b\xd1\x87\x05\xee\x97\xd6\xe0"
+ "\x5b\x43\x83\x81\x1c\x02\xf5\x41\x80\x80\x7f\xef\xa4\x61\xcf\xbc\x84\xb5"
+ "\xa8",
+ 91},
+ {"Cloudflare 'Nimbus2024' Log", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 1, // operated by Cloudflare
+ "\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\x77\xb1\x9b\x7b\x8f\xe6\x8b\x35\xfe"
+ "\x3a\x92\x29\x2d\xac\x8a\x8d\x51\x8a\x25\xfc\x93\xb6\xd7\xa0\x8b\x29\x37"
+ "\x71\x1d\x33\xca\xcc\x33\xea\x28\xb9\x1f\xe2\xac\xc3\xa9\x5d\xdd\x97\xbe"
+ "\xf6\x9e\x94\x25\xdd\x36\x81\xd1\xeb\x5d\x29\xc3\x2b\x44\xf1\x5b\xca\x15"
+ "\x48",
+ 91},
+ {"DigiCert Yeti2023 Log", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 2, // 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\x7d\x0d\x03\xb1\xd5\x98\x8a\xdc\xf0"
+ "\x15\x3b\xc6\xdc\x5e\x0d\x6e\x3f\x0d\xbf\x95\xc8\x55\x8c\xd0\xa6\x4c\x96"
+ "\xb1\x4e\x27\xb9\x26\x25\x99\xcc\x2b\x02\x9e\xa6\xd3\xdd\x9f\xb1\xd5\xc4"
+ "\xc3\xac\x35\x04\x07\x87\x97\x36\xaa\xad\x28\x0d\x7f\x2b\xd9\x68\x9f\x72"
+ "\xd1",
+ 91},
+ {"DigiCert Yeti2024 Log", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 2, // 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\x57\xb8\xc1\x6f\x30\xa4\x7f\x2e\xe4"
+ "\xf0\xd0\xd9\x60\x62\x13\x95\xe3\x7a\xe3\x4e\x53\xc3\xb3\xb8\x73\x85\xc1"
+ "\x18\x0d\x23\x0e\x58\x84\xd2\x78\xef\x9b\xb3\x1e\x2c\x1a\xde\xc1\x8f\x81"
+ "\x1b\x19\x44\x58\xb7\x00\x77\x60\x20\x1a\x72\xd8\x82\xde\xae\x9e\xb1\xc6"
+ "\x4b",
+ 91},
+ {"DigiCert Yeti2025 Log", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 2, // 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\xdf\x95\x00\x5e\x10\xc1\x01\xf7\x37"
+ "\xe3\x10\x74\xd1\xff\xb2\xca\x90\xed\x32\x99\x5f\x0c\x39\xfe\xa1\xd1\x13"
+ "\x11\xac\xd1\xb3\x73\x93\x20\xc2\x13\x3c\x4c\xb5\x7a\x52\x86\x86\x3d\xe3"
+ "\x95\x24\x7c\xd8\x91\x98\x48\x3b\xf0\xf0\xdf\x21\xf1\xb0\x81\x5a\x59\x25"
+ "\x43",
+ 91},
+ {"DigiCert Nessie2023 Log", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 2, // 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\x11\x7b\xbc\x89\x0c\x12\x09\x14\x9f"
+ "\xd8\x26\xc8\x4c\x6a\x54\xa4\x1b\x45\x56\xdf\x3e\x23\x42\x14\xd1\xdd\x42"
+ "\xdf\xa2\xdf\x7b\x5f\x9f\x6f\x07\x5a\x23\x46\x79\x16\x4b\x5f\x33\x67\xc1"
+ "\xa0\x8d\x5b\x5c\x17\x75\xf2\x4d\xa0\x80\xa1\x98\x1a\x07\x59\x06\x02\xca"
+ "\x4e",
+ 91},
+ {"DigiCert Nessie2024 Log", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 2, // 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\x2d\xfc\xa2\x7b\x36\xbf\x56\x91\xe9"
+ "\xfe\x3f\xe8\x3d\xfc\xc3\xa7\xe0\x61\x52\xea\x2c\xe9\x05\xa3\x9f\x27\x17"
+ "\x81\x05\x70\x6b\x81\x61\x44\x8a\xf8\x3b\x10\x80\x42\xed\x03\x2f\x00\x50"
+ "\x21\xfc\x41\x54\x84\xa3\x54\xd5\x2e\xb2\x7a\x16\x4b\x2a\x1f\x2b\x66\x04"
+ "\x2b",
+ 91},
+ {"DigiCert Nessie2025 Log", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 2, // 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\xf2\xf0\xf0\xa7\x8b\x81\x2e\x09\x39"
+ "\x3b\x9f\x42\xda\x38\x44\x5f\xb4\xcc\xed\x36\xbb\xd8\x43\x7f\x16\x49\x57"
+ "\x87\x04\x7f\xa5\x01\x34\xf7\xe8\x68\x3f\xb7\x78\x1f\x60\x66\x2d\x67\x9a"
+ "\x75\x80\xb7\x53\xa7\x85\xd5\xbc\xab\x47\x06\x55\xdb\xb5\xdf\x88\xa1\x6f"
+ "\x38",
+ 91},
+ {"Sectigo 'Sabre' CT log", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 3, // operated by Sectigo
+ "\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\xf2\x6f\xd2\x89\x0f\x3f\xc5\xf8\x87"
+ "\x1e\xab\x65\xb3\xd9\xbb\x17\x23\x8c\x06\x0e\x09\x55\x96\x3d\x0a\x08\xa2"
+ "\xc5\x71\xb3\xd1\xa9\x2f\x28\x3e\x83\x10\xbf\x12\xd0\x44\x66\x15\xef\x54"
+ "\xe1\x98\x80\xd0\xce\x24\x6d\x3e\x67\x9a\xe9\x37\x23\xce\x52\x93\x86\xda"
+ "\x80",
+ 91},
+ {"Sectigo 'Mammoth' CT log", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 3, // operated by Sectigo
+ "\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\xef\xe4\x7d\x74\x2e\x15\x15\xb6\xe9"
+ "\xbb\x23\x8b\xfb\x2c\xb5\xe1\xc7\x80\x98\x47\xfb\x40\x69\x68\xfc\x49\xad"
+ "\x61\x4e\x83\x47\x3c\x1a\xb7\x8d\xdf\xff\x7b\x30\xb4\xba\xff\x2f\xcb\xa0"
+ "\x14\xe3\xad\xd5\x85\x3f\x44\x59\x8c\x8c\x60\x8b\xd7\xb8\xb1\xbf\xae\x8c"
+ "\x67",
+ 91},
+ {"Let's Encrypt 'Oak2023' log", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 4, // operated by Let's Encrypt
+ "\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\xb3\x3d\x0e\x78\xbe\xe3\xad\x5c\x44"
+ "\x5c\x9b\xbe\xa3\x84\x16\x41\x82\xca\xca\x89\x17\x1e\x23\xce\x38\xa5\x54"
+ "\x2f\x7f\xd3\x34\x51\x6a\xb9\x5c\xc3\x49\xea\xfb\x91\x9d\xe0\x8a\x3c\x73"
+ "\x06\x9f\x7c\x65\x38\x11\x80\xc4\x9a\x5a\x00\xa6\x67\xc3\x83\xef\x89\x85"
+ "\x51",
+ 91},
+ {"Let's Encrypt 'Oak2024H1' log", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 4, // operated by Let's Encrypt
+ "\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\x56\x43\xd7\x7e\x7b\xd4\x72\xb7\xba"
+ "\xa9\x51\xbd\x36\x93\xb7\xe9\xb5\x92\x0f\xea\x5e\xb7\x45\xa3\x92\xfd\xc9"
+ "\xa5\x3c\x80\xac\x1a\x20\xef\x25\x2f\xb8\xe1\x20\xf7\xa8\x3a\x2e\x07\x8d"
+ "\xe6\xeb\xa4\xe2\x7d\x24\x63\x9f\x46\xbf\x94\x73\x52\x8d\x96\xae\xa9\x26"
+ "\xfd",
+ 91},
+ {"Let's Encrypt 'Oak2024H2' log", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 4, // operated by Let's Encrypt
+ "\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\x73\xd6\x53\x47\xe9\xf3\xc9\xd5"
+ "\x7c\x16\xc2\xd6\x8f\x70\x65\xfa\xf2\x51\x36\xa9\x13\x80\x2f\xed\xf9\x94"
+ "\xd3\x5a\x8b\xe8\x4f\x33\xcf\xc3\xd3\x89\xd4\x5f\x5a\x66\x89\xba\x20\x1f"
+ "\x71\xcb\xca\xbb\x9f\x9f\xf3\x5c\x2d\x1e\xa3\x81\x59\xaf\x92\xb3\x6d\x30"
+ "\x68",
+ 91},
+ {"Trust Asia Log2023", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 5, // operated by TrustAsia
+ "\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\xa4\x11\x52\xdb\x17\x41\x4e\x90\xd4"
+ "\x56\x51\x12\x30\x52\xf8\x9b\x03\xcf\x4c\x9f\xf8\x2e\x38\xb5\xf1\x5a\xba"
+ "\xfa\x38\xb9\xd2\x8f\x1a\x81\xda\x95\xcc\x33\xec\x21\x28\x66\xc6\x56\x3e"
+ "\x60\x36\x21\x20\xd9\xd4\xac\x5d\xfa\x5c\x19\xa1\x05\x7d\xfe\x20\x23\xfc"
+ "\xf5",
+ 91},
+ {"Trust Asia Log2024-2", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 5, // operated by TrustAsia
+ "\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\xa7\x64\xe2\x79\x81\x3f\x61\xd7\xec"
+ "\xc6\xf8\x65\x28\x1d\xa0\xb4\x66\x33\xc3\x25\xd5\x0a\x95\x78\x9c\x8f\xfe"
+ "\xa4\x2a\xd8\x8f\x7e\x72\xe0\xfe\xa8\x7f\xf8\xb1\x2d\x85\xc0\x8e\x12\x74"
+ "\x0d\x2f\x8c\xab\xd7\x7f\x7a\x1e\xd9\x84\x33\x39\xe8\xfd\x89\x5f\x96\x48"
+ "\x08",
+ 91},
+#ifdef DEBUG
+ {"Mozilla Test RSA Log 1", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 6, // operated by Mozilla Test Org 1
+ "\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\xba\x88\x51"
+ "\xa8\x44\x8e\x16\xd6\x41\xfd\x6e\xb6\x88\x06\x36\x10\x3d\x3c\x13\xd9\xea"
+ "\xe4\x35\x4a\xb4\xec\xf5\x68\x57\x6c\x24\x7b\xc1\xc7\x25\xa8\xe0\xd8\x1f"
+ "\xbd\xb1\x9c\x06\x9b\x6e\x1a\x86\xf2\x6b\xe2\xaf\x5a\x75\x6b\x6a\x64\x71"
+ "\x08\x7a\xa5\x5a\xa7\x45\x87\xf7\x1c\xd5\x24\x9c\x02\x7e\xcd\x43\xfc\x1e"
+ "\x69\xd0\x38\x20\x29\x93\xab\x20\xc3\x49\xe4\xdb\xb9\x4c\xc2\x6b\x6c\x0e"
+ "\xed\x15\x82\x0f\xf1\x7e\xad\x69\x1a\xb1\xd3\x02\x3a\x8b\x2a\x41\xee\xa7"
+ "\x70\xe0\x0f\x0d\x8d\xfd\x66\x0b\x2b\xb0\x24\x92\xa4\x7d\xb9\x88\x61\x79"
+ "\x90\xb1\x57\x90\x3d\xd2\x3b\xc5\xe0\xb8\x48\x1f\xa8\x37\xd3\x88\x43\xef"
+ "\x27\x16\xd8\x55\xb7\x66\x5a\xaa\x7e\x02\x90\x2f\x3a\x7b\x10\x80\x06\x24"
+ "\xcc\x1c\x6c\x97\xad\x96\x61\x5b\xb7\xe2\x96\x12\xc0\x75\x31\xa3\x0c\x91"
+ "\xdd\xb4\xca\xf7\xfc\xad\x1d\x25\xd3\x09\xef\xb9\x17\x0e\xa7\x68\xe1\xb3"
+ "\x7b\x2f\x22\x6f\x69\xe3\xb4\x8a\x95\x61\x1d\xee\x26\xd6\x25\x9d\xab\x91"
+ "\x08\x4e\x36\xcb\x1c\x24\x04\x2c\xbf\x16\x8b\x2f\xe5\xf1\x8f\x99\x17\x31"
+ "\xb8\xb3\xfe\x49\x23\xfa\x72\x51\xc4\x31\xd5\x03\xac\xda\x18\x0a\x35\xed"
+ "\x8d\x02\x03\x01\x00\x01",
+ 294},
+#endif // DEBUG
+#ifdef DEBUG
+ {"Mozilla Test EC Log", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 6, // operated by Mozilla Test Org 1
+ "\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\x4f\xbf\xbb\xbb\x61\xe0\xf8\xf9\xb1"
+ "\xa6\x0a\x59\xac\x87\x04\xe2\xec\x05\x0b\x42\x3e\x3c\xf7\x2e\x92\x3f\x2c"
+ "\x4f\x79\x4b\x45\x5c\x2a\x69\xd2\x33\x45\x6c\x36\xc4\x11\x9d\x07\x06\xe0"
+ "\x0e\xed\xc8\xd1\x93\x90\xd7\x99\x1b\x7b\x2d\x07\xa3\x04\xea\xa0\x4a\xa6"
+ "\xc0",
+ 91},
+#endif // DEBUG
+#ifdef DEBUG
+ {"Mozilla Test RSA Log 2", mozilla::ct::CTLogStatus::Included,
+ 0, // no disqualification time
+ 7, // operated by Mozilla Test Org 2
+ "\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\xc1\x75\xc6"
+ "\x52\x66\x09\x9f\x77\x08\x2a\x67\x91\xf1\xb8\x76\xc3\x7f\x5c\xe5\x38\xb0"
+ "\x6c\x4a\xcd\x22\xb1\xcb\xd4\x6f\xa6\x5a\xda\x2a\xdd\x41\xc8\xc2\x49\x8a"
+ "\xc4\xa3\xb3\xc1\xf6\x14\x87\xf4\x1b\x69\x89\x41\xbd\x80\xa5\x1c\x3c\x12"
+ "\x02\x44\xc5\x84\xa4\xc4\x48\x33\x05\xe5\x13\x8c\x01\x06\xcf\x08\xbe\x9a"
+ "\x86\x27\x60\xba\xe6\xa2\xe8\xf3\x6f\x23\xc5\xd9\x83\x13\xb9\xdf\xaf\x37"
+ "\x83\x45\xda\xce\x51\xd4\xd6\xdc\xd2\xa6\xcb\x3c\xc7\x06\xeb\xcd\x30\x70"
+ "\xec\x98\xcc\xe4\x0a\xa5\x91\xd7\x29\x5a\x7f\x71\xc5\xbe\x66\x69\x1d\x2b"
+ "\x2d\xfe\xc8\x49\x44\x59\x0b\xc5\xa3\xea\x49\xfd\x93\xb1\xd7\x53\x40\x5f"
+ "\x17\x73\x76\x99\x95\x86\x66\x25\x47\x97\xed\x42\x69\x08\x88\x08\x11\x42"
+ "\x20\x69\x98\x8a\x43\xfe\xe4\x8c\xe6\x87\x81\xdd\x22\xb6\xa6\x9c\xd2\x83"
+ "\x75\x13\x1f\x93\x2b\x12\x8c\xe2\x86\xfa\x7d\x25\x1c\x06\x2a\xd2\x7e\xf0"
+ "\x16\xf1\x87\xcd\xd5\x4e\x83\x2b\x35\xb8\x93\x0f\x74\xba\x90\xaa\x8b\xc7"
+ "\x61\x67\x24\x2a\xb1\xfd\x6d\x62\x14\x0d\x18\xc4\xc0\xb8\xc6\x8f\xc3\x74"
+ "\x84\x57\x32\x4a\xd7\xde\x86\xe6\x55\x2f\x1d\x1e\x19\x1d\x71\x21\x68\xd3"
+ "\xbb\x02\x03\x01\x00\x01",
+ 294},
+#endif // DEBUG
+};
+
+const CTLogOperatorInfo kCTLogOperatorList[] = {
+ {"Google", 0},
+ {"Cloudflare", 1},
+ {"DigiCert", 2},
+ {"Sectigo", 3},
+ {"Let's Encrypt", 4},
+ {"TrustAsia", 5},
+#ifdef DEBUG
+ {"Mozilla Test Org 1", 6},
+#endif // DEBUG
+#ifdef DEBUG
+ {"Mozilla Test Org 2", 7},
+#endif // DEBUG
+};
+
+#endif // CTKnownLogs_h
diff --git a/security/ct/CTLog.h b/security/ct/CTLog.h
new file mode 100644
index 0000000000..3d59dbd296
--- /dev/null
+++ b/security/ct/CTLog.h
@@ -0,0 +1,38 @@
+/* -*- 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>
+#include <vector>
+
+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;
+
+typedef std::vector<CTLogOperatorId> CTLogOperatorList;
+
+// 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 ct
+} // namespace mozilla
+
+#endif // CTLog_h
diff --git a/security/ct/CTLogVerifier.cpp b/security/ct/CTLogVerifier.cpp
new file mode 100644
index 0000000000..6727192622
--- /dev/null
+++ b/security/ct/CTLogVerifier.cpp
@@ -0,0 +1,305 @@
+/* -*- 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 "mozpkix/pkixnss.h"
+#include "mozpkix/pkixutil.h"
+
+namespace mozilla {
+namespace ct {
+
+using namespace mozilla::pkix;
+
+// A TrustDomain used to extract the SCT log signature parameters
+// given its subjectPublicKeyInfo.
+// Only RSASSA-PKCS1v15 with SHA-256 and ECDSA (using the NIST P-256 curve)
+// with SHA-256 are allowed.
+// RSA keys must be at least 2048 bits.
+// See See RFC 6962, Section 2.1.4.
+class SignatureParamsTrustDomain final : public TrustDomain {
+ public:
+ SignatureParamsTrustDomain()
+ : mSignatureAlgorithm(DigitallySigned::SignatureAlgorithm::Anonymous) {}
+
+ Result GetCertTrust(EndEntityOrCA, const CertPolicyId&, Input,
+ TrustLevel&) override {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ Result FindIssuer(Input, IssuerChecker&, Time) override {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ Result CheckRevocation(EndEntityOrCA, const CertID&, Time, Duration,
+ const Input*, const Input*, const Input*) override {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ Result IsChainValid(const DERArray&, Time, const CertPolicyId&) override {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ Result DigestBuf(Input, DigestAlgorithm, uint8_t*, size_t) override {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ Result CheckSignatureDigestAlgorithm(DigestAlgorithm, EndEntityOrCA,
+ Time) override {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ Result CheckECDSACurveIsAcceptable(EndEntityOrCA, NamedCurve curve) override {
+ assert(mSignatureAlgorithm ==
+ DigitallySigned::SignatureAlgorithm::Anonymous);
+ if (curve != NamedCurve::secp256r1) {
+ return Result::ERROR_UNSUPPORTED_ELLIPTIC_CURVE;
+ }
+ mSignatureAlgorithm = DigitallySigned::SignatureAlgorithm::ECDSA;
+ return Success;
+ }
+
+ Result VerifyECDSASignedData(Input, DigestAlgorithm, Input, Input) override {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ Result CheckRSAPublicKeyModulusSizeInBits(
+ EndEntityOrCA, unsigned int modulusSizeInBits) override {
+ assert(mSignatureAlgorithm ==
+ DigitallySigned::SignatureAlgorithm::Anonymous);
+ // Require RSA keys of at least 2048 bits. See RFC 6962, Section 2.1.4.
+ if (modulusSizeInBits < 2048) {
+ return Result::ERROR_INADEQUATE_KEY_SIZE;
+ }
+ mSignatureAlgorithm = DigitallySigned::SignatureAlgorithm::RSA;
+ return Success;
+ }
+
+ Result VerifyRSAPKCS1SignedData(Input, DigestAlgorithm, Input,
+ Input) override {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ Result VerifyRSAPSSSignedData(Input, DigestAlgorithm, Input, Input) override {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ Result CheckValidityIsAcceptable(Time, Time, EndEntityOrCA,
+ KeyPurposeId) override {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ Result NetscapeStepUpMatchesServerAuth(Time, bool&) override {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ void NoteAuxiliaryExtension(AuxiliaryExtension, Input) override {}
+
+ DigitallySigned::SignatureAlgorithm mSignatureAlgorithm;
+};
+
+CTLogVerifier::CTLogVerifier()
+ : mSignatureAlgorithm(DigitallySigned::SignatureAlgorithm::Anonymous),
+ mOperatorId(-1),
+ mDisqualified(false),
+ mDisqualificationTime(UINT64_MAX) {}
+
+Result 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:
+ assert(false);
+ return Result::FATAL_ERROR_INVALID_ARGS;
+ }
+
+ SignatureParamsTrustDomain trustDomain;
+ Result rv = CheckSubjectPublicKeyInfo(subjectPublicKeyInfo, trustDomain,
+ EndEntityOrCA::MustBeEndEntity);
+ if (rv != Success) {
+ return rv;
+ }
+ mSignatureAlgorithm = trustDomain.mSignatureAlgorithm;
+
+ InputToBuffer(subjectPublicKeyInfo, mSubjectPublicKeyInfo);
+
+ if (mSignatureAlgorithm == DigitallySigned::SignatureAlgorithm::ECDSA) {
+ SECItem spkiSECItem = {
+ siBuffer, mSubjectPublicKeyInfo.data(),
+ static_cast<unsigned int>(mSubjectPublicKeyInfo.size())};
+ UniqueCERTSubjectPublicKeyInfo spki(
+ SECKEY_DecodeDERSubjectPublicKeyInfo(&spkiSECItem));
+ if (!spki) {
+ return MapPRErrorCodeToResult(PR_GetError());
+ }
+ mPublicECKey.reset(SECKEY_ExtractPublicKey(spki.get()));
+ if (!mPublicECKey) {
+ return MapPRErrorCodeToResult(PR_GetError());
+ }
+ UniquePK11SlotInfo slot(PK11_GetInternalSlot());
+ if (!slot) {
+ return MapPRErrorCodeToResult(PR_GetError());
+ }
+ CK_OBJECT_HANDLE handle =
+ PK11_ImportPublicKey(slot.get(), mPublicECKey.get(), false);
+ if (handle == CK_INVALID_HANDLE) {
+ return MapPRErrorCodeToResult(PR_GetError());
+ }
+ } else {
+ mPublicECKey.reset(nullptr);
+ }
+
+ mKeyId.resize(SHA256_LENGTH);
+ rv = DigestBufNSS(subjectPublicKeyInfo, DigestAlgorithm::sha256,
+ mKeyId.data(), mKeyId.size());
+ if (rv != Success) {
+ return rv;
+ }
+
+ mOperatorId = operatorId;
+ return Success;
+}
+
+Result CTLogVerifier::Verify(const LogEntry& entry,
+ const SignedCertificateTimestamp& sct) {
+ if (mKeyId.empty() || sct.logId != mKeyId) {
+ return Result::FATAL_ERROR_INVALID_ARGS;
+ }
+ if (!SignatureParametersMatch(sct.signature)) {
+ return Result::FATAL_ERROR_INVALID_ARGS;
+ }
+
+ Buffer serializedLogEntry;
+ Result rv = EncodeLogEntry(entry, serializedLogEntry);
+ if (rv != Success) {
+ return rv;
+ }
+
+ Input logEntryInput;
+ rv = BufferToInput(serializedLogEntry, logEntryInput);
+ if (rv != Success) {
+ return rv;
+ }
+
+ // sct.extensions may be empty. If it is, sctExtensionsInput will remain in
+ // its default state, which is valid but of length 0.
+ Input sctExtensionsInput;
+ if (!sct.extensions.empty()) {
+ rv = sctExtensionsInput.Init(sct.extensions.data(), sct.extensions.size());
+ if (rv != Success) {
+ return rv;
+ }
+ }
+
+ Buffer serializedData;
+ rv = EncodeV1SCTSignedData(sct.timestamp, logEntryInput, sctExtensionsInput,
+ serializedData);
+ if (rv != Success) {
+ return rv;
+ }
+ return VerifySignature(serializedData, sct.signature.signatureData);
+}
+
+bool CTLogVerifier::SignatureParametersMatch(const DigitallySigned& signature) {
+ return signature.SignatureParametersMatch(
+ DigitallySigned::HashAlgorithm::SHA256, mSignatureAlgorithm);
+}
+
+static Result FasterVerifyECDSASignedDataNSS(Input data, Input signature,
+ UniqueSECKEYPublicKey& pubkey) {
+ assert(pubkey);
+ if (!pubkey) {
+ return Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+ // The signature is encoded as a DER SEQUENCE of two INTEGERs. PK11_Verify
+ // expects the signature as only the two integers r and s (so no encoding -
+ // just two series of bytes each half as long as SECKEY_SignatureLen(pubkey)).
+ // DSAU_DecodeDerSigToLen converts from the former format to the latter.
+ SECItem derSignatureSECItem(UnsafeMapInputToSECItem(signature));
+ size_t signatureLen = SECKEY_SignatureLen(pubkey.get());
+ if (signatureLen == 0) {
+ return MapPRErrorCodeToResult(PR_GetError());
+ }
+ UniqueSECItem signatureSECItem(
+ DSAU_DecodeDerSigToLen(&derSignatureSECItem, signatureLen));
+ if (!signatureSECItem) {
+ return MapPRErrorCodeToResult(PR_GetError());
+ }
+ SECItem dataSECItem(UnsafeMapInputToSECItem(data));
+ SECStatus srv =
+ PK11_VerifyWithMechanism(pubkey.get(), CKM_ECDSA_SHA256, nullptr,
+ signatureSECItem.get(), &dataSECItem, nullptr);
+ if (srv != SECSuccess) {
+ return MapPRErrorCodeToResult(PR_GetError());
+ }
+ return Success;
+}
+
+Result CTLogVerifier::VerifySignature(Input data, Input signature) {
+ Input spki;
+ Result rv = BufferToInput(mSubjectPublicKeyInfo, spki);
+ if (rv != Success) {
+ return rv;
+ }
+
+ switch (mSignatureAlgorithm) {
+ case DigitallySigned::SignatureAlgorithm::RSA:
+ rv = VerifyRSAPKCS1SignedDataNSS(data, DigestAlgorithm::sha256, signature,
+ spki, nullptr);
+ break;
+ case DigitallySigned::SignatureAlgorithm::ECDSA:
+ rv = FasterVerifyECDSASignedDataNSS(data, signature, mPublicECKey);
+ break;
+ // We do not expect new values added to this enum any time soon,
+ // so just listing all the available ones seems to be the easiest way
+ // to suppress warning C4061 on MSVC (which expects all values of the
+ // enum to be explicitly handled).
+ case DigitallySigned::SignatureAlgorithm::Anonymous:
+ case DigitallySigned::SignatureAlgorithm::DSA:
+ default:
+ assert(false);
+ return Result::FATAL_ERROR_INVALID_ARGS;
+ }
+ if (rv != Success) {
+ if (IsFatalError(rv)) {
+ return rv;
+ }
+ // If the error is non-fatal, we assume the signature was invalid.
+ return Result::ERROR_BAD_SIGNATURE;
+ }
+ return Success;
+}
+
+Result CTLogVerifier::VerifySignature(const Buffer& data,
+ const Buffer& signature) {
+ Input dataInput;
+ Result rv = BufferToInput(data, dataInput);
+ if (rv != Success) {
+ return rv;
+ }
+ Input signatureInput;
+ rv = BufferToInput(signature, signatureInput);
+ if (rv != Success) {
+ return rv;
+ }
+ return VerifySignature(dataInput, signatureInput);
+}
+
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/CTLogVerifier.h b/security/ct/CTLogVerifier.h
new file mode 100644
index 0000000000..659698b4a1
--- /dev/null
+++ b/security/ct/CTLogVerifier.h
@@ -0,0 +1,88 @@
+/* -*- 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 CTLogVerifier_h
+#define CTLogVerifier_h
+
+#include <memory>
+
+#include "CTLog.h"
+#include "CTUtils.h"
+#include "SignedCertificateTimestamp.h"
+#include "mozpkix/Input.h"
+#include "mozpkix/Result.h"
+#include "mozpkix/pkix.h"
+
+namespace mozilla {
+namespace ct {
+
+// Verifies Signed Certificate Timestamps (SCTs) provided by a specific log
+// using the public key of that log. Assumes the SCT being verified
+// matches the log by log key ID and signature parameters (an error is returned
+// otherwise).
+// 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. 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,
+ 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);
+
+ // Returns true if the signature and hash algorithms in |signature|
+ // match those of the log.
+ bool SignatureParametersMatch(const DigitallySigned& signature);
+
+ private:
+ // Performs the underlying verification using the log's public key. Note
+ // that |signature| contains the raw signature data (i.e. without any
+ // DigitallySigned struct encoding).
+ // 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);
+
+ // mPublicECKey works around an architectural deficiency in NSS. In the case
+ // of EC, if we don't create, import, and cache this key, NSS will import and
+ // verify it every signature verification, which is slow. For RSA, this is
+ // unused and will be null.
+ UniqueSECKEYPublicKey mPublicECKey;
+ Buffer mSubjectPublicKeyInfo;
+ Buffer mKeyId;
+ DigitallySigned::SignatureAlgorithm mSignatureAlgorithm;
+ CTLogOperatorId mOperatorId;
+ bool mDisqualified;
+ uint64_t mDisqualificationTime;
+};
+
+} // namespace ct
+} // namespace mozilla
+
+#endif // CTLogVerifier_h
diff --git a/security/ct/CTObjectsExtractor.cpp b/security/ct/CTObjectsExtractor.cpp
new file mode 100644
index 0000000000..7285bf90d5
--- /dev/null
+++ b/security/ct/CTObjectsExtractor.cpp
@@ -0,0 +1,378 @@
+/* -*- 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 "CTObjectsExtractor.h"
+
+#include <limits>
+#include <vector>
+
+#include "hasht.h"
+#include "mozpkix/pkixnss.h"
+#include "mozpkix/pkixutil.h"
+
+namespace mozilla {
+namespace ct {
+
+using namespace mozilla::pkix;
+
+// Holds a non-owning pointer to a byte buffer and allows writing chunks of data
+// to the buffer, placing the later chunks after the earlier ones
+// in a stream-like fashion.
+// Note that writing to Output always succeeds. If the internal buffer
+// overflows, an error flag is turned on and you won't be able to retrieve
+// the final data.
+class Output {
+ public:
+ Output(uint8_t* buffer, size_t length)
+ : begin(buffer),
+ end(buffer + length),
+ current(begin),
+ overflowed(false) {}
+
+ template <size_t N>
+ explicit Output(uint8_t (&buffer)[N]) : Output(buffer, N) {}
+
+ void Write(Input data) { Write(data.UnsafeGetData(), data.GetLength()); }
+
+ void Write(uint8_t b) { Write(&b, 1); }
+
+ bool IsOverflowed() const { return overflowed; }
+
+ Result GetInput(/*out*/ Input& input) const {
+ if (overflowed || current < begin) {
+ return Result::FATAL_ERROR_INVALID_STATE;
+ }
+ size_t length = static_cast<size_t>(current - begin);
+ return input.Init(begin, length);
+ }
+
+ private:
+ uint8_t* begin;
+ uint8_t* end;
+ uint8_t* current;
+ bool overflowed;
+
+ Output(const Output&) = delete;
+ void operator=(const Output&) = delete;
+
+ void Write(const uint8_t* data, size_t length) {
+ if (end < current) {
+ overflowed = true;
+ }
+ size_t available = static_cast<size_t>(end - current);
+ if (available < length) {
+ overflowed = true;
+ }
+ if (overflowed) {
+ return;
+ }
+ memcpy(current, data, length);
+ current += length;
+ }
+};
+
+// For reference:
+//
+// Certificate ::= SEQUENCE {
+// tbsCertificate TBSCertificate,
+// signatureAlgorithm AlgorithmIdentifier,
+// signatureValue BIT STRING }
+//
+// TBSCertificate ::= SEQUENCE {
+// version [0] EXPLICIT Version DEFAULT v1,
+// serialNumber CertificateSerialNumber,
+// signature AlgorithmIdentifier,
+// issuer Name,
+// validity Validity,
+// subject Name,
+// subjectPublicKeyInfo SubjectPublicKeyInfo,
+// issuerUniqueID [1] IMPLICIT UniqueIdentifier OPTIONAL,
+// -- If present, version MUST be v2 or v3
+// subjectUniqueID [2] IMPLICIT UniqueIdentifier OPTIONAL,
+// -- If present, version MUST be v2 or v3
+// extensions [3] EXPLICIT Extensions OPTIONAL
+// -- If present, version MUST be v3
+// }
+
+// python DottedOIDToCode.py id-embeddedSctList 1.3.6.1.4.1.11129.2.4.2
+// See Section 3.3 of RFC 6962.
+static const uint8_t EMBEDDED_SCT_LIST_OID[] = {0x2b, 0x06, 0x01, 0x04, 0x01,
+ 0xd6, 0x79, 0x02, 0x04, 0x02};
+// Maximum length of DER TLV header
+static const size_t MAX_TLV_HEADER_LENGTH = 4;
+// DER tag of the "extensions [3]" field from TBSCertificate
+static const uint8_t EXTENSIONS_CONTEXT_TAG =
+ der::CONTEXT_SPECIFIC | der::CONSTRUCTED | 3;
+
+Result CheckForInputSizeTypeOverflow(size_t length) {
+ if (length > std::numeric_limits<Input::size_type>::max()) {
+ return Result::FATAL_ERROR_INVALID_STATE;
+ }
+ return Success;
+}
+
+// Given a leaf certificate, extracts the DER-encoded TBSCertificate component
+// of the corresponding Precertificate.
+// Basically, the extractor needs to remove the embedded SCTs extension
+// from the certificate and return its TBSCertificate. We do it in an ad hoc
+// manner by breaking the source DER into several parts and then joining
+// the right parts, taking care to update the relevant TLV headers.
+// See WriteOutput for more details on the parts involved.
+class PrecertTBSExtractor {
+ public:
+ // |buffer| is the buffer to be used for writing the output. Since the
+ // required buffer size is not generally known in advance, it's best
+ // to use at least the size of the input certificate DER.
+ PrecertTBSExtractor(Input der, uint8_t* buffer, size_t bufferLength)
+ : mDER(der), mOutput(buffer, bufferLength) {}
+
+ // Performs the extraction.
+ Result Init() {
+ Reader tbsReader;
+ Result rv = GetTBSCertificate(tbsReader);
+ if (rv != Success) {
+ return rv;
+ }
+
+ rv = ExtractTLVsBeforeExtensions(tbsReader);
+ if (rv != Success) {
+ return rv;
+ }
+
+ rv = ExtractOptionalExtensionsExceptSCTs(tbsReader);
+ if (rv != Success) {
+ return rv;
+ }
+
+ return WriteOutput();
+ }
+
+ // Use to retrieve the result after a successful call to Init.
+ // The returned Input points to the buffer supplied in the constructor.
+ Input GetPrecertTBS() { return mPrecertTBS; }
+
+ private:
+ Result GetTBSCertificate(Reader& tbsReader) {
+ Reader certificateReader;
+ Result rv =
+ der::ExpectTagAndGetValueAtEnd(mDER, der::SEQUENCE, certificateReader);
+ if (rv != Success) {
+ return rv;
+ }
+ return ExpectTagAndGetValue(certificateReader, der::SEQUENCE, tbsReader);
+ }
+
+ Result ExtractTLVsBeforeExtensions(Reader& tbsReader) {
+ Reader::Mark tbsBegin = tbsReader.GetMark();
+ while (!tbsReader.AtEnd()) {
+ if (tbsReader.Peek(EXTENSIONS_CONTEXT_TAG)) {
+ break;
+ }
+ uint8_t tag;
+ Input tagValue;
+ Result rv = der::ReadTagAndGetValue(tbsReader, tag, tagValue);
+ if (rv != Success) {
+ return rv;
+ }
+ }
+ return tbsReader.GetInput(tbsBegin, mTLVsBeforeExtensions);
+ }
+
+ Result ExtractOptionalExtensionsExceptSCTs(Reader& tbsReader) {
+ if (!tbsReader.Peek(EXTENSIONS_CONTEXT_TAG)) {
+ return Success;
+ }
+
+ Reader extensionsContextReader;
+ Result rv = der::ExpectTagAndGetValueAtEnd(
+ tbsReader, EXTENSIONS_CONTEXT_TAG, extensionsContextReader);
+ if (rv != Success) {
+ return rv;
+ }
+
+ Reader extensionsReader;
+ rv = der::ExpectTagAndGetValueAtEnd(extensionsContextReader, der::SEQUENCE,
+ extensionsReader);
+ if (rv != Success) {
+ return rv;
+ }
+
+ while (!extensionsReader.AtEnd()) {
+ Reader::Mark extensionTLVBegin = extensionsReader.GetMark();
+ Reader extension;
+ rv =
+ der::ExpectTagAndGetValue(extensionsReader, der::SEQUENCE, extension);
+ if (rv != Success) {
+ return rv;
+ }
+ Reader extensionID;
+ rv = der::ExpectTagAndGetValue(extension, der::OIDTag, extensionID);
+ if (rv != Success) {
+ return rv;
+ }
+ if (!extensionID.MatchRest(EMBEDDED_SCT_LIST_OID)) {
+ Input extensionTLV;
+ rv = extensionsReader.GetInput(extensionTLVBegin, extensionTLV);
+ if (rv != Success) {
+ return rv;
+ }
+ mExtensionTLVs.push_back(std::move(extensionTLV));
+ }
+ }
+ return Success;
+ }
+
+ Result WriteOutput() {
+ // What should be written here:
+ //
+ // TBSCertificate ::= SEQUENCE (TLV with header |tbsHeader|)
+ // dump of |mTLVsBeforeExtensions|
+ // extensions [3] OPTIONAL (TLV with header |extensionsContextHeader|)
+ // SEQUENCE (TLV with with header |extensionsHeader|)
+ // dump of |mExtensionTLVs|
+
+ Result rv;
+ if (!mExtensionTLVs.empty()) {
+ uint8_t tbsHeaderBuffer[MAX_TLV_HEADER_LENGTH];
+ uint8_t extensionsContextHeaderBuffer[MAX_TLV_HEADER_LENGTH];
+ uint8_t extensionsHeaderBuffer[MAX_TLV_HEADER_LENGTH];
+
+ Input tbsHeader;
+ Input extensionsContextHeader;
+ Input extensionsHeader;
+
+ // Count the total size of the extensions. Note that since
+ // the extensions data is contained within mDER (an Input),
+ // their combined length won't overflow Input::size_type.
+ Input::size_type extensionsValueLength = 0;
+ for (auto& extensionTLV : mExtensionTLVs) {
+ extensionsValueLength += extensionTLV.GetLength();
+ }
+
+ rv = MakeTLVHeader(der::SEQUENCE, extensionsValueLength,
+ extensionsHeaderBuffer, extensionsHeader);
+ if (rv != Success) {
+ return rv;
+ }
+ // Since we're getting these extensions from a certificate that has
+ // already fit in an Input, this shouldn't overflow.
+ size_t extensionsContextLengthAsSizeT =
+ static_cast<size_t>(extensionsHeader.GetLength()) +
+ static_cast<size_t>(extensionsValueLength);
+ rv = CheckForInputSizeTypeOverflow(extensionsContextLengthAsSizeT);
+ if (rv != Success) {
+ return rv;
+ }
+ Input::size_type extensionsContextLength =
+ static_cast<Input::size_type>(extensionsContextLengthAsSizeT);
+ rv =
+ MakeTLVHeader(EXTENSIONS_CONTEXT_TAG, extensionsContextLength,
+ extensionsContextHeaderBuffer, extensionsContextHeader);
+ if (rv != Success) {
+ return rv;
+ }
+ size_t tbsLengthAsSizeT =
+ static_cast<size_t>(mTLVsBeforeExtensions.GetLength()) +
+ static_cast<size_t>(extensionsContextHeader.GetLength()) +
+ static_cast<size_t>(extensionsHeader.GetLength()) +
+ static_cast<size_t>(extensionsValueLength);
+ rv = CheckForInputSizeTypeOverflow(tbsLengthAsSizeT);
+ if (rv != Success) {
+ return rv;
+ }
+ Input::size_type tbsLength =
+ static_cast<Input::size_type>(tbsLengthAsSizeT);
+ rv = MakeTLVHeader(der::SEQUENCE, tbsLength, tbsHeaderBuffer, tbsHeader);
+ if (rv != Success) {
+ return rv;
+ }
+
+ mOutput.Write(tbsHeader);
+ mOutput.Write(mTLVsBeforeExtensions);
+ mOutput.Write(extensionsContextHeader);
+ mOutput.Write(extensionsHeader);
+ for (auto& extensionTLV : mExtensionTLVs) {
+ mOutput.Write(extensionTLV);
+ }
+ } else {
+ uint8_t tbsHeaderBuffer[MAX_TLV_HEADER_LENGTH];
+ Input tbsHeader;
+ rv = MakeTLVHeader(der::SEQUENCE, mTLVsBeforeExtensions.GetLength(),
+ tbsHeaderBuffer, tbsHeader);
+ if (rv != Success) {
+ return rv;
+ }
+ mOutput.Write(tbsHeader);
+ mOutput.Write(mTLVsBeforeExtensions);
+ }
+
+ return mOutput.GetInput(mPrecertTBS);
+ }
+
+ Result MakeTLVHeader(uint8_t tag, size_t length,
+ uint8_t (&buffer)[MAX_TLV_HEADER_LENGTH],
+ /*out*/ Input& header) {
+ Output output(buffer);
+ output.Write(tag);
+ if (length < 128) {
+ output.Write(static_cast<uint8_t>(length));
+ } else if (length < 256) {
+ output.Write(0x81u);
+ output.Write(static_cast<uint8_t>(length));
+ } else if (length < 65536) {
+ output.Write(0x82u);
+ output.Write(static_cast<uint8_t>(length / 256));
+ output.Write(static_cast<uint8_t>(length % 256));
+ } else {
+ return Result::FATAL_ERROR_INVALID_ARGS;
+ }
+ return output.GetInput(header);
+ }
+
+ Input mDER;
+ Input mTLVsBeforeExtensions;
+ std::vector<Input> mExtensionTLVs;
+ Output mOutput;
+ Input mPrecertTBS;
+};
+
+Result GetPrecertLogEntry(Input leafCertificate,
+ Input issuerSubjectPublicKeyInfo, LogEntry& output) {
+ assert(leafCertificate.GetLength() > 0);
+ assert(issuerSubjectPublicKeyInfo.GetLength() > 0);
+ output.Reset();
+
+ Buffer precertTBSBuffer;
+ precertTBSBuffer.resize(leafCertificate.GetLength());
+
+ PrecertTBSExtractor extractor(leafCertificate, precertTBSBuffer.data(),
+ precertTBSBuffer.size());
+ Result rv = extractor.Init();
+ if (rv != Success) {
+ return rv;
+ }
+ Input precertTBS(extractor.GetPrecertTBS());
+ assert(precertTBS.UnsafeGetData() == precertTBSBuffer.data());
+ assert(precertTBS.GetLength() <= precertTBSBuffer.size());
+ precertTBSBuffer.resize(precertTBS.GetLength());
+
+ output.type = LogEntry::Type::Precert;
+ output.tbsCertificate = std::move(precertTBSBuffer);
+
+ output.issuerKeyHash.resize(SHA256_LENGTH);
+ return DigestBufNSS(issuerSubjectPublicKeyInfo, DigestAlgorithm::sha256,
+ output.issuerKeyHash.data(), output.issuerKeyHash.size());
+}
+
+void GetX509LogEntry(Input leafCertificate, LogEntry& output) {
+ assert(leafCertificate.GetLength() > 0);
+ output.Reset();
+ output.type = LogEntry::Type::X509;
+ InputToBuffer(leafCertificate, output.leafCertificate);
+}
+
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/CTObjectsExtractor.h b/security/ct/CTObjectsExtractor.h
new file mode 100644
index 0000000000..c759307da7
--- /dev/null
+++ b/security/ct/CTObjectsExtractor.h
@@ -0,0 +1,45 @@
+/* -*- 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 CTObjectsExtractor_h
+#define CTObjectsExtractor_h
+
+#include "mozpkix/Input.h"
+#include "mozpkix/Result.h"
+#include "SignedCertificateTimestamp.h"
+
+namespace mozilla {
+namespace ct {
+
+// Obtains a PrecertChain log entry for |leafCertificate|, a DER-encoded
+// X.509v3 certificate that contains an X.509v3 extension with the
+// OID 1.3.6.1.4.1.11129.2.4.2.
+// |issuerSubjectPublicKeyInfo| is a DER-encoded SPKI of |leafCertificate|'s
+// issuer.
+// On success, fills |output| with the data for a PrecertChain log entry.
+// If |leafCertificate| does not contain the required extension,
+// an error is returned.
+// The returned |output| is intended to be verified by CTLogVerifier::Verify.
+// Note that |leafCertificate| is not checked for validity or well-formedness.
+// You might want to validate it first using pkix::BuildCertChain or similar.
+pkix::Result GetPrecertLogEntry(pkix::Input leafCertificate,
+ pkix::Input issuerSubjectPublicKeyInfo,
+ LogEntry& output);
+
+// Obtains an X509Chain log entry for |leafCertificate|, a DER-encoded
+// X.509v3 certificate that is not expected to contain an X.509v3 extension
+// with the OID 1.3.6.1.4.1.11129.2.4.2 (meaning a certificate without
+// an embedded SCT).
+// Fills |output| with the data for an X509Chain log entry.
+// The returned |output| is intended to be verified by CTLogVerifier::Verify.
+// Note that |leafCertificate| is not checked for validity or well-formedness.
+// You might want to validate it first using pkix::BuildCertChain or similar.
+void GetX509LogEntry(pkix::Input leafCertificate, LogEntry& output);
+
+} // namespace ct
+} // namespace mozilla
+
+#endif // CTObjectsExtractor_h
diff --git a/security/ct/CTPolicyEnforcer.cpp b/security/ct/CTPolicyEnforcer.cpp
new file mode 100644
index 0000000000..0cacb25da6
--- /dev/null
+++ b/security/ct/CTPolicyEnforcer.cpp
@@ -0,0 +1,277 @@
+/* -*- 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 "CTPolicyEnforcer.h"
+
+#include <algorithm>
+#include <stdint.h>
+#include <vector>
+
+namespace mozilla {
+namespace ct {
+
+using namespace mozilla::pkix;
+
+// Returns the number of embedded SCTs required to be present on a
+// certificate for Qualification Case #2 (embedded SCTs).
+static size_t GetRequiredEmbeddedSctsCount(
+ size_t certLifetimeInFullCalendarMonths) {
+ // "there are Embedded SCTs from AT LEAST N+1 once or currently qualified
+ // logs, where N is the lifetime of the certificate in years (normally
+ // rounding up, but rounding down when up to 3 months over), and must be
+ // at least 1"
+ return 1 + (certLifetimeInFullCalendarMonths + 9) / 12;
+}
+
+// Whether a valid embedded SCT is present in the list.
+static bool HasValidEmbeddedSct(const VerifiedSCTList& verifiedScts) {
+ for (const VerifiedSCT& verifiedSct : verifiedScts) {
+ if (verifiedSct.status == VerifiedSCT::Status::Valid &&
+ verifiedSct.origin == VerifiedSCT::Origin::Embedded) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// Whether a valid non-embedded SCT is present in the list.
+static bool HasValidNonEmbeddedSct(const VerifiedSCTList& verifiedScts) {
+ for (const VerifiedSCT& verifiedSct : verifiedScts) {
+ if (verifiedSct.status == VerifiedSCT::Status::Valid &&
+ (verifiedSct.origin == VerifiedSCT::Origin::TLSExtension ||
+ verifiedSct.origin == VerifiedSCT::Origin::OCSPResponse)) {
+ return true;
+ }
+ }
+ return false;
+}
+
+// Given a list of verified SCTs, counts the number of distinct CA-independent
+// log operators running the CT logs that issued the SCTs which satisfy
+// the provided boolean predicate.
+template <typename SelectFunc>
+void CountIndependentLogOperatorsForSelectedScts(
+ const VerifiedSCTList& verifiedScts,
+ const CTLogOperatorList& dependentOperators, size_t& count,
+ SelectFunc selected) {
+ CTLogOperatorList operatorIds;
+ for (const VerifiedSCT& verifiedSct : verifiedScts) {
+ CTLogOperatorId sctLogOperatorId = verifiedSct.logOperatorId;
+ // Check if |sctLogOperatorId| is CA-dependent.
+ bool isDependentOperator = false;
+ for (CTLogOperatorId dependentOperator : dependentOperators) {
+ if (sctLogOperatorId == dependentOperator) {
+ isDependentOperator = true;
+ break;
+ }
+ }
+ if (isDependentOperator || !selected(verifiedSct)) {
+ continue;
+ }
+ // Check if |sctLogOperatorId| is in |operatorIds|...
+ bool alreadyAdded = false;
+ for (CTLogOperatorId id : operatorIds) {
+ if (id == sctLogOperatorId) {
+ alreadyAdded = true;
+ break;
+ }
+ }
+ // ...and if not, add it.
+ if (!alreadyAdded) {
+ operatorIds.push_back(sctLogOperatorId);
+ }
+ }
+ count = operatorIds.size();
+}
+
+// Given a list of verified SCTs, counts the number of distinct CT logs
+// that issued the SCTs that satisfy the |selected| predicate.
+template <typename SelectFunc>
+static void CountLogsForSelectedScts(const VerifiedSCTList& verifiedScts,
+ size_t& count, SelectFunc selected) {
+ // Keep pointers to log ids (of type Buffer) from |verifiedScts| to save on
+ // memory allocations.
+ std::vector<const Buffer*> logIds;
+ for (const VerifiedSCT& verifiedSct : verifiedScts) {
+ if (!selected(verifiedSct)) {
+ continue;
+ }
+
+ const Buffer* sctLogId = &verifiedSct.sct.logId;
+ // Check if |sctLogId| points to data already in |logIds|...
+ bool alreadyAdded = false;
+ for (const Buffer* logId : logIds) {
+ if (*logId == *sctLogId) {
+ alreadyAdded = true;
+ break;
+ }
+ }
+ // ...and if not, add it.
+ if (!alreadyAdded) {
+ logIds.push_back(sctLogId);
+ }
+ }
+ count = logIds.size();
+}
+
+// Calculates the effective issuance time of connection's certificate using
+// the SCTs present on the connection (we can't rely on notBefore validity
+// field of the certificate since it can be backdated).
+// Used to determine whether to accept SCTs issued by past qualified logs.
+// The effective issuance time is defined as the earliest of all SCTs,
+// rather than the latest of embedded SCTs, in order to give CAs the benefit
+// of the doubt in the event a log is revoked in the midst of processing
+// a precertificate and issuing the certificate.
+// It is acceptable to ignore the origin of the SCTs because SCTs
+// delivered via OCSP/TLS extension will cover the full certificate,
+// which necessarily will exist only after the precertificate
+// has been logged and the actual certificate issued.
+static uint64_t GetEffectiveCertIssuanceTime(
+ const VerifiedSCTList& verifiedScts) {
+ uint64_t result = UINT64_MAX;
+ for (const VerifiedSCT& verifiedSct : verifiedScts) {
+ if (verifiedSct.status == VerifiedSCT::Status::Valid) {
+ result = std::min(result, verifiedSct.sct.timestamp);
+ }
+ }
+ return result;
+}
+
+// Checks if the log that issued the given SCT is "once or currently qualified"
+// (i.e. was qualified at the time of the certificate issuance). In addition,
+// makes sure the SCT is before the disqualification.
+static bool LogWasQualifiedForSct(const VerifiedSCT& verifiedSct,
+ uint64_t certIssuanceTime) {
+ if (verifiedSct.status == VerifiedSCT::Status::Valid) {
+ return true;
+ }
+ if (verifiedSct.status == VerifiedSCT::Status::ValidFromDisqualifiedLog) {
+ uint64_t logDisqualificationTime = verifiedSct.logDisqualificationTime;
+ return certIssuanceTime < logDisqualificationTime &&
+ verifiedSct.sct.timestamp < logDisqualificationTime;
+ }
+ return false;
+}
+
+// "A certificate is CT Qualified if it is presented with at least two SCTs
+// from once or currently qualified logs run by a minimum of two entities
+// independent of the CA and of each other."
+// By the preexisting certificate exception provision (not currently
+// implemented), certificates "are CT Qualified if they are presented with SCTs
+// from once or currently qualified logs run by a minimum of one entity
+// independent of the CA."
+static void CheckOperatorDiversityCompliance(
+ const VerifiedSCTList& verifiedScts, uint64_t certIssuanceTime,
+ const CTLogOperatorList& dependentOperators, bool& compliant) {
+ size_t independentOperatorsCount;
+ CountIndependentLogOperatorsForSelectedScts(
+ verifiedScts, dependentOperators, independentOperatorsCount,
+ [certIssuanceTime](const VerifiedSCT& verifiedSct) -> bool {
+ return LogWasQualifiedForSct(verifiedSct, certIssuanceTime);
+ });
+ // Having at least 2 operators implies we have at least 2 SCTs.
+ // For the preexisting certificate exception provision (1 operator) we will
+ // need to include an additional SCTs count check using
+ // CountLogsForSelectedScts(verifiedScts, sctsCount,
+ // [certIssuanceTime](const VerifiedSCT& verifiedSct)->bool {
+ // return LogWasQualifiedForSct(verifiedSct, certIssuanceTime);
+ // });
+ compliant = independentOperatorsCount >= 2;
+}
+
+// Qualification Case #1 (non-embedded SCTs) - the following must hold:
+// a. An SCT from a log qualified at the time of check is presented via the
+// TLS extension OR is embedded within a stapled OCSP response;
+// AND
+// b. There are at least two SCTs from logs qualified at the time of check,
+// presented via any method.
+static void CheckNonEmbeddedCompliance(const VerifiedSCTList& verifiedScts,
+ bool& compliant) {
+ if (!HasValidNonEmbeddedSct(verifiedScts)) {
+ compliant = false;
+ return;
+ }
+
+ size_t validSctsCount;
+ CountLogsForSelectedScts(
+ verifiedScts, validSctsCount, [](const VerifiedSCT& verifiedSct) -> bool {
+ return verifiedSct.status == VerifiedSCT::Status::Valid;
+ });
+
+ compliant = validSctsCount >= 2;
+}
+
+// Qualification Case #2 (embedded SCTs) - the following must hold:
+// a. An Embedded SCT from a log qualified at the time of check is presented;
+// AND
+// b. There are Embedded SCTs from AT LEAST N + 1 once or currently qualified
+// logs, where N is the lifetime of the certificate in years (normally
+// rounding up, but rounding down when up to 3 months over), and must be
+// at least 1.
+static void CheckEmbeddedCompliance(const VerifiedSCTList& verifiedScts,
+ size_t certLifetimeInCalendarMonths,
+ uint64_t certIssuanceTime,
+ bool& compliant) {
+ if (!HasValidEmbeddedSct(verifiedScts)) {
+ compliant = false;
+ return;
+ }
+
+ // Count the compliant embedded SCTs. Only a single SCT from each log
+ // is accepted. Note that a given log might return several different SCTs
+ // for the same precertificate (it is permitted, but advised against).
+ size_t embeddedSctsCount;
+ CountLogsForSelectedScts(
+ verifiedScts, embeddedSctsCount,
+ [certIssuanceTime](const VerifiedSCT& verifiedSct) -> bool {
+ return verifiedSct.origin == VerifiedSCT::Origin::Embedded &&
+ LogWasQualifiedForSct(verifiedSct, certIssuanceTime);
+ });
+
+ size_t requiredSctsCount =
+ GetRequiredEmbeddedSctsCount(certLifetimeInCalendarMonths);
+
+ compliant = embeddedSctsCount >= requiredSctsCount;
+}
+
+void CTPolicyEnforcer::CheckCompliance(
+ const VerifiedSCTList& verifiedScts, size_t certLifetimeInCalendarMonths,
+ const CTLogOperatorList& dependentOperators,
+ CTPolicyCompliance& compliance) {
+ uint64_t certIssuanceTime = GetEffectiveCertIssuanceTime(verifiedScts);
+
+ bool diversityOK;
+ CheckOperatorDiversityCompliance(verifiedScts, certIssuanceTime,
+ dependentOperators, diversityOK);
+
+ bool nonEmbeddedCaseOK;
+ CheckNonEmbeddedCompliance(verifiedScts, nonEmbeddedCaseOK);
+
+ bool embeddedCaseOK;
+ CheckEmbeddedCompliance(verifiedScts, certLifetimeInCalendarMonths,
+ certIssuanceTime, embeddedCaseOK);
+
+ if (nonEmbeddedCaseOK || embeddedCaseOK) {
+ compliance = diversityOK ? CTPolicyCompliance::Compliant
+ : CTPolicyCompliance::NotDiverseScts;
+ } else {
+ // Not enough SCTs are present to satisfy either case of the policy.
+ compliance = CTPolicyCompliance::NotEnoughScts;
+ }
+
+ switch (compliance) {
+ case CTPolicyCompliance::Compliant:
+ case CTPolicyCompliance::NotEnoughScts:
+ case CTPolicyCompliance::NotDiverseScts:
+ break;
+ case CTPolicyCompliance::Unknown:
+ default:
+ assert(false);
+ }
+}
+
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/CTPolicyEnforcer.h b/security/ct/CTPolicyEnforcer.h
new file mode 100644
index 0000000000..0cbbec642f
--- /dev/null
+++ b/security/ct/CTPolicyEnforcer.h
@@ -0,0 +1,65 @@
+/* -*- 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 CTPolicyEnforcer_h
+#define CTPolicyEnforcer_h
+
+#include "CTLog.h"
+#include "CTVerifyResult.h"
+#include "mozpkix/Result.h"
+
+namespace mozilla {
+namespace ct {
+
+// Information about the compliance of the TLS connection with the
+// Certificate Transparency policy.
+enum class CTPolicyCompliance {
+ // Compliance not checked or not applicable.
+ Unknown,
+ // The connection complied with the certificate policy
+ // by including SCTs that satisfy the policy.
+ Compliant,
+ // The connection did not have enough valid SCTs to comply.
+ NotEnoughScts,
+ // The connection had enough valid SCTs, but the diversity requirement
+ // was not met (the number of CT log operators independent of the CA
+ // and of each other is too low).
+ NotDiverseScts,
+};
+
+// Checks whether a TLS connection complies with the current CT policy.
+// The implemented policy is based on the Mozilla CT Policy draft 0.1.0
+// (see https://docs.google.com/document/d/
+// 1rnqYYwscAx8WhS-MCdTiNzYQus9e37HuVyafQvEeNro/edit).
+//
+// NOTE: CT DIVERSITY REQUIREMENT IS TBD, PENDING FINALIZATION
+// OF MOZILLA CT POLICY. Specifically:
+// 1. CT log operators being CA-dependent is not currently taken into account
+// (see CTDiversityPolicy.h).
+// 2. The preexisting certificate exception provision of the operator diversity
+// requirement is not implemented (see "CT Qualified" section of the policy and
+// CheckOperatorDiversityCompliance in CTPolicyEnforcer.cpp).
+class CTPolicyEnforcer {
+ public:
+ // |verifiedSct| - SCTs present on the connection along with their
+ // verification status.
+ // |certLifetimeInCalendarMonths| - certificate lifetime in full calendar
+ // months (i.e. rounded down), based on the notBefore/notAfter fields.
+ // |dependentOperators| - which CT log operators are dependent on the CA
+ // that issued the certificate. SCTs issued by logs associated with such
+ // operators are treated differenly when evaluating the policy.
+ // See CTDiversityPolicy class.
+ // |compliance| - the result of the compliance check.
+ void CheckCompliance(const VerifiedSCTList& verifiedScts,
+ size_t certLifetimeInCalendarMonths,
+ const CTLogOperatorList& dependentOperators,
+ CTPolicyCompliance& compliance);
+};
+
+} // namespace ct
+} // namespace mozilla
+
+#endif // CTPolicyEnforcer_h
diff --git a/security/ct/CTSerialization.cpp b/security/ct/CTSerialization.cpp
new file mode 100644
index 0000000000..ed6cbb891a
--- /dev/null
+++ b/security/ct/CTSerialization.cpp
@@ -0,0 +1,396 @@
+/* -*- 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 "CTSerialization.h"
+#include "CTUtils.h"
+
+#include <stdint.h>
+#include <type_traits>
+
+namespace mozilla {
+namespace ct {
+
+using namespace mozilla::pkix;
+
+typedef mozilla::pkix::Result Result;
+
+// Note: length is always specified in bytes.
+// Signed Certificate Timestamp (SCT) Version length
+static const size_t kVersionLength = 1;
+
+// Members of a V1 SCT
+static const size_t kLogIdLength = 32;
+static const size_t kTimestampLength = 8;
+static const size_t kExtensionsLengthBytes = 2;
+static const size_t kHashAlgorithmLength = 1;
+static const size_t kSigAlgorithmLength = 1;
+static const size_t kSignatureLengthBytes = 2;
+
+// Members of the digitally-signed struct of a V1 SCT
+static const size_t kSignatureTypeLength = 1;
+static const size_t kLogEntryTypeLength = 2;
+static const size_t kAsn1CertificateLengthBytes = 3;
+static const size_t kTbsCertificateLengthBytes = 3;
+
+static const size_t kSCTListLengthBytes = 2;
+static const size_t kSerializedSCTLengthBytes = 2;
+
+enum class SignatureType {
+ CertificateTimestamp = 0,
+ TreeHash = 1,
+};
+
+// Reads a serialized hash algorithm.
+static Result ReadHashAlgorithm(Reader& in,
+ DigitallySigned::HashAlgorithm& out) {
+ unsigned int value;
+ Result rv = ReadUint<kHashAlgorithmLength>(in, value);
+ if (rv != Success) {
+ return rv;
+ }
+ DigitallySigned::HashAlgorithm algo =
+ static_cast<DigitallySigned::HashAlgorithm>(value);
+ switch (algo) {
+ case DigitallySigned::HashAlgorithm::None:
+ case DigitallySigned::HashAlgorithm::MD5:
+ case DigitallySigned::HashAlgorithm::SHA1:
+ case DigitallySigned::HashAlgorithm::SHA224:
+ case DigitallySigned::HashAlgorithm::SHA256:
+ case DigitallySigned::HashAlgorithm::SHA384:
+ case DigitallySigned::HashAlgorithm::SHA512:
+ out = algo;
+ return Success;
+ }
+ return Result::ERROR_BAD_DER;
+}
+
+// Reads a serialized signature algorithm.
+static Result ReadSignatureAlgorithm(Reader& in,
+ DigitallySigned::SignatureAlgorithm& out) {
+ unsigned int value;
+ Result rv = ReadUint<kSigAlgorithmLength>(in, value);
+ if (rv != Success) {
+ return rv;
+ }
+ DigitallySigned::SignatureAlgorithm algo =
+ static_cast<DigitallySigned::SignatureAlgorithm>(value);
+ switch (algo) {
+ case DigitallySigned::SignatureAlgorithm::Anonymous:
+ case DigitallySigned::SignatureAlgorithm::RSA:
+ case DigitallySigned::SignatureAlgorithm::DSA:
+ case DigitallySigned::SignatureAlgorithm::ECDSA:
+ out = algo;
+ return Success;
+ }
+ return Result::ERROR_BAD_DER;
+}
+
+// Reads a serialized version enum.
+static Result ReadVersion(Reader& in,
+ SignedCertificateTimestamp::Version& out) {
+ unsigned int value;
+ Result rv = ReadUint<kVersionLength>(in, value);
+ if (rv != Success) {
+ return rv;
+ }
+ SignedCertificateTimestamp::Version version =
+ static_cast<SignedCertificateTimestamp::Version>(value);
+ switch (version) {
+ case SignedCertificateTimestamp::Version::V1:
+ out = version;
+ return Success;
+ }
+ return Result::ERROR_BAD_DER;
+}
+
+// Writes a TLS-encoded variable length unsigned integer to |output|.
+// Note: range/overflow checks are not performed on the input parameters.
+// |length| indicates the size (in bytes) of the integer to be written.
+// |value| the value itself to be written.
+static Result UncheckedWriteUint(size_t length, uint64_t value,
+ Buffer& output) {
+ output.reserve(length + output.size());
+ for (; length > 0; --length) {
+ uint8_t nextByte = (value >> ((length - 1) * 8)) & 0xFF;
+ output.push_back(nextByte);
+ }
+ return Success;
+}
+
+// Performs sanity checks on T and calls UncheckedWriteUint.
+template <size_t length, typename T>
+static inline Result WriteUint(T value, Buffer& output) {
+ static_assert(length <= 8, "At most 8 byte integers can be written");
+ static_assert(sizeof(T) >= length, "T must be able to hold <length> bytes");
+ if (std::is_signed<T>::value) {
+ // We accept signed integer types assuming the actual value is non-negative.
+ if (value < 0) {
+ return Result::FATAL_ERROR_INVALID_ARGS;
+ }
+ }
+ if (sizeof(T) > length) {
+ // We allow the value variable to take more bytes than is written,
+ // but the unwritten bytes must be zero.
+ // Note: when "sizeof(T) == length" holds, "value >> (length * 8)" is
+ // undefined since the shift is too big. On some compilers, this would
+ // produce a warning even though the actual code is unreachable.
+ if (value >> (length * 8 - 1) > 1) {
+ return Result::FATAL_ERROR_INVALID_ARGS;
+ }
+ }
+ return UncheckedWriteUint(length, static_cast<uint64_t>(value), output);
+}
+
+// Writes an array to |output| from |input|.
+// Should be used in one of two cases:
+// * The length of |input| has already been encoded into the |output| stream.
+// * The length of |input| is fixed and the reader is expected to specify that
+// length when reading.
+// If the length of |input| is dynamic and data is expected to follow it,
+// WriteVariableBytes must be used.
+static void WriteEncodedBytes(Input input, Buffer& output) {
+ output.insert(output.end(), input.UnsafeGetData(),
+ input.UnsafeGetData() + input.GetLength());
+}
+
+// Same as above, but the source data is in a Buffer.
+static void WriteEncodedBytes(const Buffer& source, Buffer& output) {
+ output.insert(output.end(), source.begin(), source.end());
+}
+
+// A variable-length byte array is prefixed by its length when serialized.
+// This writes the length prefix.
+// |prefixLength| indicates the number of bytes needed to represent the length.
+// |dataLength| is the length of the byte array following the prefix.
+// Fails if |dataLength| is more than 2^|prefixLength| - 1.
+template <size_t prefixLength>
+static Result WriteVariableBytesPrefix(size_t dataLength, Buffer& output) {
+ const size_t maxAllowedInputSize =
+ static_cast<size_t>(((1 << (prefixLength * 8)) - 1));
+ if (dataLength > maxAllowedInputSize) {
+ return Result::FATAL_ERROR_INVALID_ARGS;
+ }
+
+ return WriteUint<prefixLength>(dataLength, output);
+}
+
+// Writes a variable-length array to |output|.
+// |prefixLength| indicates the number of bytes needed to represent the length.
+// |input| is the array itself.
+// Fails if the size of |input| is more than 2^|prefixLength| - 1.
+template <size_t prefixLength>
+static Result WriteVariableBytes(Input input, Buffer& output) {
+ Result rv = WriteVariableBytesPrefix<prefixLength>(input.GetLength(), output);
+ if (rv != Success) {
+ return rv;
+ }
+ WriteEncodedBytes(input, output);
+ return Success;
+}
+
+// Same as above, but the source data is in a Buffer.
+template <size_t prefixLength>
+static Result WriteVariableBytes(const Buffer& source, Buffer& output) {
+ Input input;
+ Result rv = BufferToInput(source, input);
+ if (rv != Success) {
+ return rv;
+ }
+ return WriteVariableBytes<prefixLength>(input, output);
+}
+
+// Writes a LogEntry of type X.509 cert to |output|.
+// |input| is the LogEntry containing the certificate.
+static Result EncodeAsn1CertLogEntry(const LogEntry& entry, Buffer& output) {
+ return WriteVariableBytes<kAsn1CertificateLengthBytes>(entry.leafCertificate,
+ output);
+}
+
+// Writes a LogEntry of type PreCertificate to |output|.
+// |input| is the LogEntry containing the TBSCertificate and issuer key hash.
+static Result EncodePrecertLogEntry(const LogEntry& entry, Buffer& output) {
+ if (entry.issuerKeyHash.size() != kLogIdLength) {
+ return Result::FATAL_ERROR_INVALID_ARGS;
+ }
+ WriteEncodedBytes(entry.issuerKeyHash, output);
+ return WriteVariableBytes<kTbsCertificateLengthBytes>(entry.tbsCertificate,
+ output);
+}
+
+Result EncodeDigitallySigned(const DigitallySigned& data, Buffer& output) {
+ Result rv = WriteUint<kHashAlgorithmLength>(
+ static_cast<unsigned int>(data.hashAlgorithm), output);
+ if (rv != Success) {
+ return rv;
+ }
+ rv = WriteUint<kSigAlgorithmLength>(
+ static_cast<unsigned int>(data.signatureAlgorithm), output);
+ if (rv != Success) {
+ return rv;
+ }
+ return WriteVariableBytes<kSignatureLengthBytes>(data.signatureData, output);
+}
+
+Result DecodeDigitallySigned(Reader& reader, DigitallySigned& output) {
+ DigitallySigned result;
+
+ Result rv = ReadHashAlgorithm(reader, result.hashAlgorithm);
+ if (rv != Success) {
+ return rv;
+ }
+ rv = ReadSignatureAlgorithm(reader, result.signatureAlgorithm);
+ if (rv != Success) {
+ return rv;
+ }
+
+ Input signatureData;
+ rv = ReadVariableBytes<kSignatureLengthBytes>(reader, signatureData);
+ if (rv != Success) {
+ return rv;
+ }
+ InputToBuffer(signatureData, result.signatureData);
+
+ output = std::move(result);
+ return Success;
+}
+
+Result EncodeLogEntry(const LogEntry& entry, Buffer& output) {
+ Result rv = WriteUint<kLogEntryTypeLength>(
+ static_cast<unsigned int>(entry.type), output);
+ if (rv != Success) {
+ return rv;
+ }
+ switch (entry.type) {
+ case LogEntry::Type::X509:
+ return EncodeAsn1CertLogEntry(entry, output);
+ case LogEntry::Type::Precert:
+ return EncodePrecertLogEntry(entry, output);
+ default:
+ assert(false);
+ }
+ return Result::ERROR_BAD_DER;
+}
+
+static Result WriteTimeSinceEpoch(uint64_t timestamp, Buffer& output) {
+ return WriteUint<kTimestampLength>(timestamp, output);
+}
+
+Result EncodeV1SCTSignedData(uint64_t timestamp, Input serializedLogEntry,
+ Input extensions, Buffer& output) {
+ Result rv = WriteUint<kVersionLength>(
+ static_cast<unsigned int>(SignedCertificateTimestamp::Version::V1),
+ output);
+ if (rv != Success) {
+ return rv;
+ }
+ rv = WriteUint<kSignatureTypeLength>(
+ static_cast<unsigned int>(SignatureType::CertificateTimestamp), output);
+ if (rv != Success) {
+ return rv;
+ }
+ rv = WriteTimeSinceEpoch(timestamp, output);
+ if (rv != Success) {
+ return rv;
+ }
+ // NOTE: serializedLogEntry must already be serialized and contain the
+ // length as the prefix.
+ WriteEncodedBytes(serializedLogEntry, output);
+ return WriteVariableBytes<kExtensionsLengthBytes>(extensions, output);
+}
+
+Result DecodeSCTList(Input input, Reader& listReader) {
+ Reader inputReader(input);
+ Input listData;
+ Result rv = ReadVariableBytes<kSCTListLengthBytes>(inputReader, listData);
+ if (rv != Success) {
+ return rv;
+ }
+ return listReader.Init(listData);
+}
+
+Result ReadSCTListItem(Reader& listReader, Input& output) {
+ if (listReader.AtEnd()) {
+ return Result::FATAL_ERROR_INVALID_ARGS;
+ }
+
+ Result rv = ReadVariableBytes<kSerializedSCTLengthBytes>(listReader, output);
+ if (rv != Success) {
+ return rv;
+ }
+ if (output.GetLength() == 0) {
+ return Result::ERROR_BAD_DER;
+ }
+ return Success;
+}
+
+Result DecodeSignedCertificateTimestamp(Reader& reader,
+ SignedCertificateTimestamp& output) {
+ SignedCertificateTimestamp result;
+
+ Result rv = ReadVersion(reader, result.version);
+ if (rv != Success) {
+ return rv;
+ }
+
+ uint64_t timestamp;
+ Input logId;
+ Input extensions;
+
+ rv = ReadFixedBytes(kLogIdLength, reader, logId);
+ if (rv != Success) {
+ return rv;
+ }
+ rv = ReadUint<kTimestampLength>(reader, timestamp);
+ if (rv != Success) {
+ return rv;
+ }
+ rv = ReadVariableBytes<kExtensionsLengthBytes>(reader, extensions);
+ if (rv != Success) {
+ return rv;
+ }
+ rv = DecodeDigitallySigned(reader, result.signature);
+ if (rv != Success) {
+ return rv;
+ }
+
+ InputToBuffer(logId, result.logId);
+ InputToBuffer(extensions, result.extensions);
+ result.timestamp = timestamp;
+
+ output = std::move(result);
+ return Success;
+}
+
+Result EncodeSCTList(const std::vector<pkix::Input>& scts, Buffer& output) {
+ // Find out the total size of the SCT list to be written so we can
+ // write the prefix for the list before writing its contents.
+ size_t sctListLength = 0;
+ for (auto& sct : scts) {
+ sctListLength +=
+ /* data size */ sct.GetLength() +
+ /* length prefix size */ kSerializedSCTLengthBytes;
+ }
+
+ output.reserve(kSCTListLengthBytes + sctListLength);
+
+ // Write the prefix for the SCT list.
+ Result rv =
+ WriteVariableBytesPrefix<kSCTListLengthBytes>(sctListLength, output);
+ if (rv != Success) {
+ return rv;
+ }
+ // Now write each SCT from the list.
+ for (auto& sct : scts) {
+ rv = WriteVariableBytes<kSerializedSCTLengthBytes>(sct, output);
+ if (rv != Success) {
+ return rv;
+ }
+ }
+ return Success;
+}
+
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/CTSerialization.h b/security/ct/CTSerialization.h
new file mode 100644
index 0000000000..67b7c5f50c
--- /dev/null
+++ b/security/ct/CTSerialization.h
@@ -0,0 +1,65 @@
+/* -*- 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 CTSerialization_h
+#define CTSerialization_h
+
+#include <vector>
+
+#include "mozpkix/Input.h"
+#include "mozpkix/Result.h"
+#include "SignedCertificateTimestamp.h"
+
+// Utility functions for encoding/decoding structures used by Certificate
+// Transparency to/from the TLS wire format encoding.
+namespace mozilla {
+namespace ct {
+
+// Encodes the DigitallySigned |data| to |output|.
+pkix::Result EncodeDigitallySigned(const DigitallySigned& data, Buffer& output);
+
+// Reads and decodes a DigitallySigned object from |reader|.
+// On failure, the cursor position of |reader| is undefined.
+pkix::Result DecodeDigitallySigned(pkix::Reader& reader,
+ DigitallySigned& output);
+
+// Encodes the |input| LogEntry to |output|. The size of the entry
+// must not exceed the allowed size in RFC6962.
+pkix::Result EncodeLogEntry(const LogEntry& entry, Buffer& output);
+
+// Encodes the data signed by a Signed Certificate Timestamp (SCT) into
+// |output|. The signature included in the SCT can then be verified over these
+// bytes.
+// |timestamp| timestamp from the SCT.
+// |serializedLogEntry| the log entry signed by the SCT.
+// |extensions| CT extensions.
+pkix::Result EncodeV1SCTSignedData(uint64_t timestamp,
+ pkix::Input serializedLogEntry,
+ pkix::Input extensions, Buffer& output);
+
+// Decodes a list of Signed Certificate Timestamps
+// (SignedCertificateTimestampList as defined in RFC6962). This list
+// is typically obtained from the CT extension in a certificate.
+// To extract the individual items of the list, call ReadSCTListItem on
+// the returned reader until the reader reaches its end.
+// Note that the validity of each extracted SCT should be checked separately.
+pkix::Result DecodeSCTList(pkix::Input input, pkix::Reader& listReader);
+
+// Reads a single SCT from the reader returned by DecodeSCTList.
+pkix::Result ReadSCTListItem(pkix::Reader& listReader, pkix::Input& result);
+
+// Decodes a single SCT from |input| to |output|.
+pkix::Result DecodeSignedCertificateTimestamp(
+ pkix::Reader& input, SignedCertificateTimestamp& output);
+
+// Encodes a list of SCTs (|scts|) to |output|.
+pkix::Result EncodeSCTList(const std::vector<pkix::Input>& scts,
+ Buffer& output);
+
+} // namespace ct
+} // namespace mozilla
+
+#endif // CTSerialization_h
diff --git a/security/ct/CTUtils.h b/security/ct/CTUtils.h
new file mode 100644
index 0000000000..68ddd927f0
--- /dev/null
+++ b/security/ct/CTUtils.h
@@ -0,0 +1,111 @@
+/* -*- 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 CTUtils_h
+#define CTUtils_h
+
+#include <memory>
+
+#include "cryptohi.h"
+#include "keyhi.h"
+#include "keythi.h"
+#include "pk11pub.h"
+#include "mozpkix/Input.h"
+#include "mozpkix/Result.h"
+
+#define MOZILLA_CT_ARRAY_LENGTH(x) (sizeof(x) / sizeof((x)[0]))
+
+struct DeleteHelper {
+ void operator()(CERTSubjectPublicKeyInfo* value) {
+ SECKEY_DestroySubjectPublicKeyInfo(value);
+ }
+ void operator()(PK11Context* value) { PK11_DestroyContext(value, true); }
+ void operator()(PK11SlotInfo* value) { PK11_FreeSlot(value); }
+ void operator()(SECKEYPublicKey* value) { SECKEY_DestroyPublicKey(value); }
+ void operator()(SECItem* value) { SECITEM_FreeItem(value, true); }
+};
+
+template <class T>
+struct MaybeDeleteHelper {
+ void operator()(T* ptr) {
+ if (ptr) {
+ DeleteHelper del;
+ del(ptr);
+ }
+ }
+};
+
+typedef std::unique_ptr<CERTSubjectPublicKeyInfo,
+ MaybeDeleteHelper<CERTSubjectPublicKeyInfo>>
+ UniqueCERTSubjectPublicKeyInfo;
+typedef std::unique_ptr<PK11Context, MaybeDeleteHelper<PK11Context>>
+ UniquePK11Context;
+typedef std::unique_ptr<PK11SlotInfo, MaybeDeleteHelper<PK11SlotInfo>>
+ UniquePK11SlotInfo;
+typedef std::unique_ptr<SECKEYPublicKey, MaybeDeleteHelper<SECKEYPublicKey>>
+ UniqueSECKEYPublicKey;
+typedef std::unique_ptr<SECItem, MaybeDeleteHelper<SECItem>> UniqueSECItem;
+
+namespace mozilla {
+namespace ct {
+
+// Reads a TLS-encoded variable length unsigned integer from |in|.
+// The integer is expected to be in big-endian order, which is used by TLS.
+// Note: does not check if the output parameter overflows while reading.
+// |length| indicates the size (in bytes) of the serialized integer.
+inline static pkix::Result UncheckedReadUint(size_t length, pkix::Reader& in,
+ uint64_t& out) {
+ uint64_t result = 0;
+ for (size_t i = 0; i < length; ++i) {
+ uint8_t value;
+ pkix::Result rv = in.Read(value);
+ if (rv != pkix::Success) {
+ return rv;
+ }
+ result = (result << 8) | value;
+ }
+ out = result;
+ return pkix::Success;
+}
+
+// Performs overflow sanity checks and calls UncheckedReadUint.
+template <size_t length, typename T>
+pkix::Result ReadUint(pkix::Reader& in, T& out) {
+ uint64_t value;
+ static_assert(std::is_unsigned<T>::value, "T must be unsigned");
+ static_assert(length <= 8, "At most 8 byte integers can be read");
+ static_assert(sizeof(T) >= length, "T must be able to hold <length> bytes");
+ pkix::Result rv = UncheckedReadUint(length, in, value);
+ if (rv != pkix::Success) {
+ return rv;
+ }
+ out = static_cast<T>(value);
+ return pkix::Success;
+}
+
+// Reads |length| bytes from |in|.
+static inline pkix::Result ReadFixedBytes(size_t length, pkix::Reader& in,
+ pkix::Input& out) {
+ return in.Skip(length, out);
+}
+
+// Reads a length-prefixed variable amount of bytes from |in|, updating |out|
+// on success. |prefixLength| indicates the number of bytes needed to represent
+// the length.
+template <size_t prefixLength>
+pkix::Result ReadVariableBytes(pkix::Reader& in, pkix::Input& out) {
+ size_t length;
+ pkix::Result rv = ReadUint<prefixLength>(in, length);
+ if (rv != pkix::Success) {
+ return rv;
+ }
+ return ReadFixedBytes(length, in, out);
+}
+
+} // namespace ct
+} // namespace mozilla
+
+#endif // CTUtils_h
diff --git a/security/ct/CTVerifyResult.cpp b/security/ct/CTVerifyResult.cpp
new file mode 100644
index 0000000000..72c3706186
--- /dev/null
+++ b/security/ct/CTVerifyResult.cpp
@@ -0,0 +1,26 @@
+/* -*- 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 "CTVerifyResult.h"
+
+#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;
+}
+
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/CTVerifyResult.h b/security/ct/CTVerifyResult.h
new file mode 100644
index 0000000000..ad7f4774aa
--- /dev/null
+++ b/security/ct/CTVerifyResult.h
@@ -0,0 +1,84 @@
+/* -*- 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 CTVerifyResult_h
+#define CTVerifyResult_h
+
+#include <vector>
+
+#include "CTLog.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 {
+ 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 {
+ Unknown,
+ Embedded,
+ TLSExtension,
+ OCSPResponse,
+ };
+
+ Status status;
+ Origin origin;
+ CTLogOperatorId logOperatorId;
+ uint64_t logDisqualificationTime;
+};
+
+typedef std::vector<VerifiedSCT> VerifiedSCTList;
+
+// Holds Signed Certificate Timestamps verification results.
+class CTVerifyResult {
+ public:
+ CTVerifyResult() { Reset(); }
+
+ // SCTs that were processed during the verification along with their
+ // verification results.
+ VerifiedSCTList verifiedScts;
+
+ // The verifier makes the best effort to extract the available SCTs
+ // from the binary sources provided to it.
+ // If some SCT cannot be extracted due to encoding errors, the verifier
+ // proceeds to the next available one. In other words, decoding errors are
+ // effectively ignored.
+ // Note that a serialized SCT may fail to decode for a "legitimate" reason,
+ // e.g. if the SCT is from a future version of the Certificate Transparency
+ // standard.
+ // |decodingErrors| field counts the errors of the above kind.
+ size_t decodingErrors;
+
+ void Reset();
+};
+
+} // namespace ct
+} // namespace mozilla
+
+#endif // CTVerifyResult_h
diff --git a/security/ct/MultiLogCTVerifier.cpp b/security/ct/MultiLogCTVerifier.cpp
new file mode 100644
index 0000000000..78f7abb7a5
--- /dev/null
+++ b/security/ct/MultiLogCTVerifier.cpp
@@ -0,0 +1,194 @@
+/* -*- 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 "MultiLogCTVerifier.h"
+
+#include "CTObjectsExtractor.h"
+#include "CTSerialization.h"
+
+namespace mozilla {
+namespace ct {
+
+using namespace mozilla::pkix;
+
+// Note: this moves |verifiedSct| to the target list in |result|.
+static void StoreVerifiedSct(CTVerifyResult& result, VerifiedSCT&& verifiedSct,
+ VerifiedSCT::Status status) {
+ verifiedSct.status = status;
+ result.verifiedScts.push_back(std::move(verifiedSct));
+}
+
+void MultiLogCTVerifier::AddLog(CTLogVerifier&& log) {
+ mLogs.push_back(std::move(log));
+}
+
+Result MultiLogCTVerifier::Verify(Input cert, Input issuerSubjectPublicKeyInfo,
+ Input sctListFromCert,
+ Input sctListFromOCSPResponse,
+ Input sctListFromTLSExtension, Time time,
+ CTVerifyResult& result) {
+ assert(cert.GetLength() > 0);
+ result.Reset();
+
+ Result rv;
+
+ // Verify embedded SCTs
+ if (issuerSubjectPublicKeyInfo.GetLength() > 0 &&
+ sctListFromCert.GetLength() > 0) {
+ LogEntry precertEntry;
+ rv = GetPrecertLogEntry(cert, issuerSubjectPublicKeyInfo, precertEntry);
+ if (rv != Success) {
+ return rv;
+ }
+ rv = VerifySCTs(sctListFromCert, precertEntry,
+ VerifiedSCT::Origin::Embedded, time, result);
+ if (rv != Success) {
+ return rv;
+ }
+ }
+
+ LogEntry x509Entry;
+ GetX509LogEntry(cert, x509Entry);
+
+ // Verify SCTs from a stapled OCSP response
+ if (sctListFromOCSPResponse.GetLength() > 0) {
+ rv = VerifySCTs(sctListFromOCSPResponse, x509Entry,
+ VerifiedSCT::Origin::OCSPResponse, time, result);
+ if (rv != Success) {
+ return rv;
+ }
+ }
+
+ // Verify SCTs from a TLS extension
+ if (sctListFromTLSExtension.GetLength() > 0) {
+ rv = VerifySCTs(sctListFromTLSExtension, x509Entry,
+ VerifiedSCT::Origin::TLSExtension, time, result);
+ if (rv != Success) {
+ return rv;
+ }
+ }
+ return Success;
+}
+
+void DecodeSCTs(Input encodedSctList,
+ std::vector<SignedCertificateTimestamp>& decodedSCTs,
+ size_t& decodingErrors) {
+ decodedSCTs.clear();
+
+ Reader listReader;
+ Result rv = DecodeSCTList(encodedSctList, listReader);
+ if (rv != Success) {
+ decodingErrors++;
+ return;
+ }
+
+ while (!listReader.AtEnd()) {
+ Input encodedSct;
+ rv = ReadSCTListItem(listReader, encodedSct);
+ if (rv != Success) {
+ decodingErrors++;
+ return;
+ }
+
+ Reader encodedSctReader(encodedSct);
+ SignedCertificateTimestamp sct;
+ rv = DecodeSignedCertificateTimestamp(encodedSctReader, sct);
+ if (rv != Success) {
+ decodingErrors++;
+ continue;
+ }
+ decodedSCTs.push_back(std::move(sct));
+ }
+}
+
+Result MultiLogCTVerifier::VerifySCTs(Input encodedSctList,
+ const LogEntry& expectedEntry,
+ VerifiedSCT::Origin origin, Time time,
+ CTVerifyResult& result) {
+ std::vector<SignedCertificateTimestamp> decodedSCTs;
+ DecodeSCTs(encodedSctList, decodedSCTs, result.decodingErrors);
+ for (auto sct : decodedSCTs) {
+ Result rv =
+ VerifySingleSCT(std::move(sct), expectedEntry, origin, time, result);
+ if (rv != Success) {
+ return rv;
+ }
+ }
+ return Success;
+}
+
+Result MultiLogCTVerifier::VerifySingleSCT(SignedCertificateTimestamp&& sct,
+ const LogEntry& expectedEntry,
+ VerifiedSCT::Origin origin,
+ Time time, CTVerifyResult& result) {
+ VerifiedSCT verifiedSct;
+ verifiedSct.origin = origin;
+ verifiedSct.sct = std::move(sct);
+
+ CTLogVerifier* matchingLog = nullptr;
+ for (auto& log : mLogs) {
+ if (log.keyId() == verifiedSct.sct.logId) {
+ matchingLog = &log;
+ break;
+ }
+ }
+
+ if (!matchingLog) {
+ // SCT does not match any known log.
+ StoreVerifiedSct(result, std::move(verifiedSct),
+ VerifiedSCT::Status::UnknownLog);
+ return Success;
+ }
+
+ verifiedSct.logOperatorId = matchingLog->operatorId();
+
+ if (!matchingLog->SignatureParametersMatch(verifiedSct.sct.signature)) {
+ // SCT signature parameters do not match the log's.
+ StoreVerifiedSct(result, std::move(verifiedSct),
+ VerifiedSCT::Status::InvalidSignature);
+ return Success;
+ }
+
+ Result rv = matchingLog->Verify(expectedEntry, verifiedSct.sct);
+ if (rv != Success) {
+ if (rv == Result::ERROR_BAD_SIGNATURE) {
+ StoreVerifiedSct(result, std::move(verifiedSct),
+ VerifiedSCT::Status::InvalidSignature);
+ return Success;
+ }
+ return rv;
+ }
+
+ // Make sure the timestamp is legitimate (not in the future).
+ // SCT's |timestamp| is measured in milliseconds since the epoch,
+ // ignoring leap seconds. When converting it to a second-level precision
+ // pkix::Time, we need to round it either up or down. In our case, rounding up
+ // (towards the future) is more "secure", although practically
+ // it does not matter.
+ Time sctTime =
+ TimeFromEpochInSeconds((verifiedSct.sct.timestamp + 999u) / 1000u);
+ if (sctTime > time) {
+ StoreVerifiedSct(result, std::move(verifiedSct),
+ VerifiedSCT::Status::InvalidTimestamp);
+ return Success;
+ }
+
+ // 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();
+ StoreVerifiedSct(result, std::move(verifiedSct),
+ VerifiedSCT::Status::ValidFromDisqualifiedLog);
+ return Success;
+ }
+
+ StoreVerifiedSct(result, std::move(verifiedSct), VerifiedSCT::Status::Valid);
+ return Success;
+}
+
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/MultiLogCTVerifier.h b/security/ct/MultiLogCTVerifier.h
new file mode 100644
index 0000000000..983f96677b
--- /dev/null
+++ b/security/ct/MultiLogCTVerifier.h
@@ -0,0 +1,89 @@
+/* -*- 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 MultiLogCTVerifier_h
+#define MultiLogCTVerifier_h
+
+#include <vector>
+
+#include "CTLogVerifier.h"
+#include "CTVerifyResult.h"
+#include "mozpkix/Input.h"
+#include "mozpkix/Result.h"
+#include "mozpkix/Time.h"
+#include "SignedCertificateTimestamp.h"
+
+namespace mozilla {
+namespace ct {
+
+void DecodeSCTs(pkix::Input encodedSctList,
+ std::vector<SignedCertificateTimestamp>& decodedSCTs,
+ size_t& decodingErrors);
+
+// A Certificate Transparency verifier that can verify Signed Certificate
+// Timestamps from multiple logs.
+class MultiLogCTVerifier {
+ public:
+ // Adds a new log to the list of known logs to verify against.
+ void AddLog(CTLogVerifier&& log);
+
+ // Verifies SCTs embedded in the certificate itself, SCTs embedded in a
+ // stapled OCSP response, and SCTs obtained via the
+ // signed_certificate_timestamp TLS extension on the given |cert|.
+ //
+ // A certificate is permitted but not required to use multiple sources for
+ // SCTs. It is expected that most certificates will use only one source
+ // (embedding, TLS extension or OCSP stapling).
+ //
+ // The verifier stops on fatal errors (such as out of memory or invalid
+ // DER encoding of |cert|), but it does not stop on SCT decoding errors. See
+ // CTVerifyResult for more details.
+ //
+ // The internal state of the verifier object is not modified
+ // during the verification process.
+ //
+ // |cert| DER-encoded certificate to be validated using the provided SCTs.
+ // |sctListFromCert| SCT list embedded in |cert|, empty if not present.
+ // |issuerSubjectPublicKeyInfo| SPKI of |cert|'s issuer. Can be empty,
+ // in which case the embedded SCT list
+ // won't be verified.
+ // |sctListFromOCSPResponse| SCT list included in a stapled OCSP response
+ // for |cert|. Empty if not available.
+ // |sctListFromTLSExtension| is the SCT list from the TLS extension. Empty
+ // if no extension was present.
+ // |time| the current time. Used to make sure SCTs are not in the future.
+ // |result| will be filled with the SCTs present, divided into categories
+ // based on the verification result.
+ pkix::Result Verify(pkix::Input cert, pkix::Input issuerSubjectPublicKeyInfo,
+ pkix::Input sctListFromCert,
+ pkix::Input sctListFromOCSPResponse,
+ pkix::Input sctListFromTLSExtension, pkix::Time time,
+ CTVerifyResult& result);
+
+ private:
+ // Verifies a list of SCTs from |encodedSctList| over |expectedEntry|,
+ // placing the verification results in |result|. The SCTs in the list
+ // come from |origin| (as will be reflected in the origin field of each SCT).
+ pkix::Result VerifySCTs(pkix::Input encodedSctList,
+ const LogEntry& expectedEntry,
+ VerifiedSCT::Origin origin, pkix::Time time,
+ CTVerifyResult& result);
+
+ // Verifies a single, parsed SCT against all known logs.
+ // Note: moves |sct| to the target list in |result|, invalidating |sct|.
+ pkix::Result VerifySingleSCT(SignedCertificateTimestamp&& sct,
+ const ct::LogEntry& expectedEntry,
+ VerifiedSCT::Origin origin, pkix::Time time,
+ CTVerifyResult& result);
+
+ // The list of known logs.
+ std::vector<CTLogVerifier> mLogs;
+};
+
+} // namespace ct
+} // namespace mozilla
+
+#endif // MultiLogCTVerifier_h
diff --git a/security/ct/SignedCertificateTimestamp.cpp b/security/ct/SignedCertificateTimestamp.cpp
new file mode 100644
index 0000000000..573006d599
--- /dev/null
+++ b/security/ct/SignedCertificateTimestamp.cpp
@@ -0,0 +1,27 @@
+/* -*- 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 "SignedCertificateTimestamp.h"
+
+namespace mozilla {
+namespace ct {
+
+void LogEntry::Reset() {
+ type = LogEntry::Type::X509;
+ leafCertificate.clear();
+ issuerKeyHash.clear();
+ tbsCertificate.clear();
+}
+
+bool DigitallySigned::SignatureParametersMatch(
+ HashAlgorithm aHashAlgorithm,
+ SignatureAlgorithm aSignatureAlgorithm) const {
+ return (hashAlgorithm == aHashAlgorithm) &&
+ (signatureAlgorithm == aSignatureAlgorithm);
+}
+
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/SignedCertificateTimestamp.h b/security/ct/SignedCertificateTimestamp.h
new file mode 100644
index 0000000000..2803439295
--- /dev/null
+++ b/security/ct/SignedCertificateTimestamp.h
@@ -0,0 +1,92 @@
+/* -*- 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 SignedCertificateTimestamp_h
+#define SignedCertificateTimestamp_h
+
+#include "Buffer.h"
+#include "mozpkix/Input.h"
+#include "mozpkix/Result.h"
+
+// Structures related to Certificate Transparency (RFC 6962).
+namespace mozilla {
+namespace ct {
+
+// LogEntry struct in RFC 6962, Section 3.1.
+struct LogEntry {
+ // LogEntryType enum in RFC 6962, Section 3.1.
+ enum class Type { X509 = 0, Precert = 1 };
+
+ void Reset();
+
+ Type type;
+
+ // Set if type == X509.
+ Buffer leafCertificate;
+
+ // Set if type == Precert.
+ Buffer issuerKeyHash;
+ Buffer tbsCertificate;
+};
+
+// Helper structure to represent Digitally Signed data, as described in
+// Sections 4.7 and 7.4.1.4.1 of RFC 5246.
+struct DigitallySigned {
+ enum class HashAlgorithm {
+ None = 0,
+ MD5 = 1,
+ SHA1 = 2,
+ SHA224 = 3,
+ SHA256 = 4,
+ SHA384 = 5,
+ SHA512 = 6,
+ };
+
+ enum class SignatureAlgorithm { Anonymous = 0, RSA = 1, DSA = 2, ECDSA = 3 };
+
+ // Returns true if |aHashAlgorithm| and |aSignatureAlgorithm|
+ // match this DigitallySigned hash and signature algorithms.
+ bool SignatureParametersMatch(HashAlgorithm aHashAlgorithm,
+ SignatureAlgorithm aSignatureAlgorithm) const;
+
+ HashAlgorithm hashAlgorithm;
+ SignatureAlgorithm signatureAlgorithm;
+ // 'signature' field.
+ Buffer signatureData;
+};
+
+// SignedCertificateTimestamp struct in RFC 6962, Section 3.2.
+struct SignedCertificateTimestamp {
+ // Version enum in RFC 6962, Section 3.2.
+ enum class Version {
+ V1 = 0,
+ };
+
+ Version version;
+ 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) {
+ if (buffer.empty()) {
+ return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+ return input.Init(buffer.data(), buffer.size());
+}
+
+inline void InputToBuffer(pkix::Input input, Buffer& buffer) {
+ buffer.assign(input.UnsafeGetData(),
+ input.UnsafeGetData() + input.GetLength());
+}
+
+} // namespace ct
+} // namespace mozilla
+
+#endif // SignedCertificateTimestamp_h
diff --git a/security/ct/moz.build b/security/ct/moz.build
new file mode 100644
index 0000000000..b9a2a62b28
--- /dev/null
+++ b/security/ct/moz.build
@@ -0,0 +1,53 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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/.
+
+with Files("**"):
+ BUG_COMPONENT = ("Core", "Security: PSM")
+
+EXPORTS += [
+ "BTTypes.h",
+ "BTVerifier.h",
+ "Buffer.h",
+ "CTLog.h",
+ "CTPolicyEnforcer.h",
+ "CTVerifyResult.h",
+ "SignedCertificateTimestamp.h",
+]
+
+UNIFIED_SOURCES += [
+ "BTVerifier.cpp",
+ "Buffer.cpp",
+ "CTDiversityPolicy.cpp",
+ "CTLogVerifier.cpp",
+ "CTObjectsExtractor.cpp",
+ "CTPolicyEnforcer.cpp",
+ "CTSerialization.cpp",
+ "CTVerifyResult.cpp",
+ "MultiLogCTVerifier.cpp",
+ "SignedCertificateTimestamp.cpp",
+]
+
+TEST_DIRS += [
+ "tests/gtest",
+]
+
+if not CONFIG["MOZ_DEBUG"]:
+ DEFINES["NDEBUG"] = True
+
+CXXFLAGS += [
+ "-Wextra",
+ "-Wunreachable-code",
+]
+
+# Gecko headers aren't warning-free enough for us to enable these warnings.
+CXXFLAGS += [
+ "-Wno-unused-parameter",
+]
+
+FINAL_LIBRARY = "xul"
+
+if CONFIG["CC_TYPE"] == "clang-cl":
+ AllowCompilerWarnings() # workaround for bug 1090497
diff --git a/security/ct/tests/gtest/BTSerializationTest.cpp b/security/ct/tests/gtest/BTSerializationTest.cpp
new file mode 100644
index 0000000000..75c16faf42
--- /dev/null
+++ b/security/ct/tests/gtest/BTSerializationTest.cpp
@@ -0,0 +1,140 @@
+/* -*- 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 "BTVerifier.h"
+#include "CTTestUtils.h"
+#include "gtest/gtest.h"
+
+namespace mozilla {
+namespace ct {
+
+using namespace pkix;
+
+class BTSerializationTest : public ::testing::Test {
+ public:
+ void SetUp() override {
+ mTestInclusionProof = GetTestInclusionProof();
+ mTestInclusionProofUnexpectedData = GetTestInclusionProofUnexpectedData();
+ mTestInclusionProofInvalidHashSize = GetTestInclusionProofInvalidHashSize();
+ mTestInclusionProofInvalidHash = GetTestInclusionProofInvalidHash();
+ mTestInclusionProofMissingLogId = GetTestInclusionProofMissingLogId();
+ mTestInclusionProofNullPathLength = GetTestInclusionProofNullPathLength();
+ mTestInclusionProofPathLengthTooSmall =
+ GetTestInclusionProofPathLengthTooSmall();
+ mTestInclusionProofPathLengthTooLarge =
+ GetTestInclusionProofPathLengthTooLarge();
+ mTestInclusionProofNullTreeSize = GetTestInclusionProofNullTreeSize();
+ mTestInclusionProofLeafIndexOutOfBounds =
+ GetTestInclusionProofLeafIndexOutOfBounds();
+ mTestInclusionProofExtraData = GetTestInclusionProofExtraData();
+ }
+
+ protected:
+ Buffer mTestInclusionProof;
+ Buffer mTestInclusionProofUnexpectedData;
+ Buffer mTestInclusionProofInvalidHashSize;
+ Buffer mTestInclusionProofInvalidHash;
+ Buffer mTestInclusionProofMissingLogId;
+ Buffer mTestInclusionProofNullPathLength;
+ Buffer mTestInclusionProofPathLengthTooSmall;
+ Buffer mTestInclusionProofPathLengthTooLarge;
+ Buffer mTestInclusionProofNullTreeSize;
+ Buffer mTestInclusionProofLeafIndexOutOfBounds;
+ Buffer mTestInclusionProofExtraData;
+};
+
+TEST_F(BTSerializationTest, DecodesInclusionProof) {
+ const uint64_t expectedTreeSize = 4;
+ const uint64_t expectedLeafIndex = 2;
+ const uint64_t expectedInclusionPathElements = 2;
+
+ Buffer expectedLogId = {0x01, 0x00};
+
+ Input encodedProofInput = InputForBuffer(mTestInclusionProof);
+ InclusionProofDataV2 ipr;
+ ASSERT_EQ(Success, DecodeInclusionProof(encodedProofInput, ipr));
+ EXPECT_EQ(expectedLogId, ipr.logId);
+ EXPECT_EQ(expectedTreeSize, ipr.treeSize);
+ EXPECT_EQ(expectedLeafIndex, ipr.leafIndex);
+ EXPECT_EQ(expectedInclusionPathElements, ipr.inclusionPath.size());
+ EXPECT_EQ(GetTestNodeHash0(), ipr.inclusionPath[0]);
+ EXPECT_EQ(GetTestNodeHash1(), ipr.inclusionPath[1]);
+}
+
+TEST_F(BTSerializationTest, FailsDecodingInclusionProofUnexpectedData) {
+ Input encodedProofInput = InputForBuffer(mTestInclusionProofUnexpectedData);
+ InclusionProofDataV2 ipr;
+ ASSERT_EQ(pkix::Result::ERROR_BAD_DER,
+ DecodeInclusionProof(encodedProofInput, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingInvalidHashSize) {
+ Input encodedProofInput = InputForBuffer(mTestInclusionProofInvalidHashSize);
+ InclusionProofDataV2 ipr;
+ ASSERT_EQ(pkix::Result::ERROR_BAD_DER,
+ DecodeInclusionProof(encodedProofInput, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingInvalidHash) {
+ Input encodedProofInput = InputForBuffer(mTestInclusionProofInvalidHash);
+ InclusionProofDataV2 ipr;
+ ASSERT_EQ(pkix::Result::ERROR_BAD_DER,
+ DecodeInclusionProof(encodedProofInput, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingMissingLogId) {
+ Input encodedProofInput = InputForBuffer(mTestInclusionProofMissingLogId);
+ InclusionProofDataV2 ipr;
+ ASSERT_EQ(pkix::Result::ERROR_BAD_DER,
+ DecodeInclusionProof(encodedProofInput, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingNullPathLength) {
+ Input encodedProofInput = InputForBuffer(mTestInclusionProofNullPathLength);
+ InclusionProofDataV2 ipr;
+ ASSERT_EQ(pkix::Result::ERROR_BAD_DER,
+ DecodeInclusionProof(encodedProofInput, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingPathLengthTooSmall) {
+ Input encodedProofInput =
+ InputForBuffer(mTestInclusionProofPathLengthTooSmall);
+ InclusionProofDataV2 ipr;
+ ASSERT_EQ(pkix::Result::ERROR_BAD_DER,
+ DecodeInclusionProof(encodedProofInput, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingPathLengthTooLarge) {
+ Input encodedProofInput =
+ InputForBuffer(mTestInclusionProofPathLengthTooLarge);
+ InclusionProofDataV2 ipr;
+ ASSERT_EQ(pkix::Result::ERROR_BAD_DER,
+ DecodeInclusionProof(encodedProofInput, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingNullTreeSize) {
+ Input encodedProofInput = InputForBuffer(mTestInclusionProofNullTreeSize);
+ InclusionProofDataV2 ipr;
+ ASSERT_EQ(pkix::Result::ERROR_BAD_DER,
+ DecodeInclusionProof(encodedProofInput, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingLeafIndexOutOfBounds) {
+ Input encodedProofInput =
+ InputForBuffer(mTestInclusionProofLeafIndexOutOfBounds);
+ InclusionProofDataV2 ipr;
+ ASSERT_EQ(pkix::Result::ERROR_BAD_DER,
+ DecodeInclusionProof(encodedProofInput, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingExtraData) {
+ Input encodedProofInput = InputForBuffer(mTestInclusionProofExtraData);
+ InclusionProofDataV2 ipr;
+ ASSERT_EQ(pkix::Result::ERROR_BAD_DER,
+ DecodeInclusionProof(encodedProofInput, ipr));
+}
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/tests/gtest/BTSignedTreeHeadTest.cpp b/security/ct/tests/gtest/BTSignedTreeHeadTest.cpp
new file mode 100644
index 0000000000..580d6a84f3
--- /dev/null
+++ b/security/ct/tests/gtest/BTSignedTreeHeadTest.cpp
@@ -0,0 +1,266 @@
+/* -*- 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 "BTVerifier.h"
+#include "CTTestUtils.h"
+#include "gtest/gtest.h"
+
+#include "nss.h"
+
+namespace mozilla {
+namespace ct {
+
+using namespace pkix;
+
+struct BTSignedTreeHeadTestParams {
+ const char* mSubjectPublicKeyInfoHex;
+ pkix::DigestAlgorithm mDigestAlgorithm;
+ pkix::der::PublicKeyAlgorithm mPublicKeyAlgorithm;
+ const char* mSignedTreeHeadHex;
+
+ pkix::Result mExpectedSignedTreeHeadResult;
+ uint64_t mExpectedTimestamp;
+ uint64_t mExpectedTreeSize;
+ const char* mExpectedRootHashHex;
+};
+
+class BTSignedTreeHeadTest
+ : public ::testing::Test,
+ public ::testing::WithParamInterface<BTSignedTreeHeadTestParams> {
+ void SetUp() override {
+ if (!NSS_IsInitialized()) {
+ if (NSS_NoDB_Init(nullptr) != SECSuccess) {
+ abort();
+ }
+ }
+ }
+};
+
+namespace ValidSTH {
+#include "valid-sth.inc"
+}
+namespace ValidWithExtensionSTH {
+#include "valid-with-extension-sth.inc"
+}
+namespace ValidSecp521r1SHA512STH {
+#include "valid-secp521r1-sha512-sth.inc"
+}
+namespace SignatureCoversLogIDSTH {
+#include "signature-covers-log-id-sth.inc"
+}
+namespace WrongSPKISTH {
+#include "wrong-spki-sth.inc"
+}
+namespace WrongSigningKeySTH {
+#include "wrong-signing-key-sth.inc"
+}
+namespace MissingLogIDSTH {
+#include "missing-log-id-sth.inc"
+}
+namespace MissingTimestampSTH {
+#include "missing-timestamp-sth.inc"
+}
+namespace MissingTreeSizeSTH {
+#include "missing-tree-size-sth.inc"
+}
+namespace MissingRootHashSTH {
+#include "missing-root-hash-sth.inc"
+}
+namespace MissingExtensionsSTH {
+#include "missing-extensions-sth.inc"
+}
+namespace TruncatedLogIDSTH {
+#include "truncated-log-id-sth.inc"
+}
+namespace TruncatedTimestampSTH {
+#include "truncated-timestamp-sth.inc"
+}
+namespace TruncatedTreeSizeSTH {
+#include "truncated-tree-size-sth.inc"
+}
+namespace TruncatedRootHashSTH {
+#include "truncated-root-hash-sth.inc"
+}
+namespace TruncatedExtensionSTH {
+#include "truncated-extension-sth.inc"
+}
+namespace RSASignerRSASPKISTH {
+#include "rsa-signer-rsa-spki-sth.inc"
+}
+namespace RSASignerECSPKISTH {
+#include "rsa-signer-ec-spki-sth.inc"
+}
+namespace ECSignerRSASPKISTH {
+#include "ec-signer-rsa-spki-sth.inc"
+}
+
+static const char* kValidRootHashHex =
+ "d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc";
+
+static const char* kValidRootHashSHA512Hex =
+ "374d794a95cdcfd8b35993185fef9ba368f160d8daf432d08ba9f1ed1e5abe6c"
+ "c69291e0fa2fe0006a52570ef18c19def4e617c33ce52ef0a6e5fbe318cb0387";
+
+static const BTSignedTreeHeadTestParams BT_SIGNED_TREE_HEAD_TEST_PARAMS[] = {
+ {ValidSTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, ValidSTH::kSTHHex, Success,
+ 1541189938000, 7, kValidRootHashHex},
+ {ValidSTH::kSPKIHex, pkix::DigestAlgorithm::sha512,
+ pkix::der::PublicKeyAlgorithm::ECDSA, ValidSTH::kSTHHex,
+ Result::ERROR_BAD_SIGNATURE, 0, 0, nullptr},
+ {ValidSTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::RSA_PKCS1, ValidSTH::kSTHHex,
+ Result::FATAL_ERROR_INVALID_ARGS, 0, 0, nullptr},
+ {ValidWithExtensionSTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, ValidWithExtensionSTH::kSTHHex,
+ Success, 1541189938000, 7, kValidRootHashHex},
+ {ValidSecp521r1SHA512STH::kSPKIHex, pkix::DigestAlgorithm::sha512,
+ pkix::der::PublicKeyAlgorithm::ECDSA, ValidSecp521r1SHA512STH::kSTHHex,
+ Success, 1542136309473, 731393445, kValidRootHashSHA512Hex},
+ {ValidSecp521r1SHA512STH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, ValidSecp521r1SHA512STH::kSTHHex,
+ Result::ERROR_BAD_SIGNATURE, 0, 0, nullptr},
+ {ValidSTH::kSPKIHex, pkix::DigestAlgorithm::sha512,
+ pkix::der::PublicKeyAlgorithm::ECDSA, ValidSecp521r1SHA512STH::kSTHHex,
+ Result::ERROR_BAD_SIGNATURE, 0, 0, nullptr},
+ {SignatureCoversLogIDSTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, SignatureCoversLogIDSTH::kSTHHex,
+ Result::ERROR_BAD_SIGNATURE, 0, 0, nullptr},
+ {WrongSPKISTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, WrongSPKISTH::kSTHHex,
+ Result::ERROR_BAD_SIGNATURE, 0, 0, nullptr},
+ {WrongSigningKeySTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, WrongSigningKeySTH::kSTHHex,
+ Result::ERROR_BAD_SIGNATURE, 0, 0, nullptr},
+ {MissingLogIDSTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, MissingLogIDSTH::kSTHHex,
+ Result::ERROR_BAD_DER, 0, 0, nullptr},
+ {MissingTimestampSTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, MissingTimestampSTH::kSTHHex,
+ Result::ERROR_BAD_DER, 0, 0, nullptr},
+ {MissingTreeSizeSTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, MissingTreeSizeSTH::kSTHHex,
+ Result::ERROR_BAD_DER, 0, 0, nullptr},
+ {MissingRootHashSTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, MissingRootHashSTH::kSTHHex,
+ Result::ERROR_BAD_DER, 0, 0, nullptr},
+ {MissingExtensionsSTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, MissingExtensionsSTH::kSTHHex,
+ Result::ERROR_BAD_DER, 0, 0, nullptr},
+ {TruncatedLogIDSTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, TruncatedLogIDSTH::kSTHHex,
+ Result::ERROR_BAD_DER, 0, 0, nullptr},
+ {TruncatedTimestampSTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, TruncatedTimestampSTH::kSTHHex,
+ Result::ERROR_BAD_DER, 0, 0, nullptr},
+ {TruncatedTreeSizeSTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, TruncatedTreeSizeSTH::kSTHHex,
+ Result::ERROR_BAD_DER, 0, 0, nullptr},
+ {TruncatedRootHashSTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, TruncatedRootHashSTH::kSTHHex,
+ Result::ERROR_BAD_DER, 0, 0, nullptr},
+ {TruncatedExtensionSTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, TruncatedExtensionSTH::kSTHHex,
+ Result::ERROR_BAD_DER, 0, 0, nullptr},
+ {RSASignerRSASPKISTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, RSASignerRSASPKISTH::kSTHHex,
+ Result::ERROR_BAD_SIGNATURE, 0, 0, nullptr},
+ {RSASignerECSPKISTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, RSASignerECSPKISTH::kSTHHex,
+ Result::ERROR_BAD_SIGNATURE, 0, 0, nullptr},
+ {ECSignerRSASPKISTH::kSPKIHex, pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA, ECSignerRSASPKISTH::kSTHHex,
+ Result::ERROR_INVALID_KEY, 0, 0, nullptr},
+};
+
+TEST_P(BTSignedTreeHeadTest, BTSignedTreeHeadSimpleTest) {
+ const BTSignedTreeHeadTestParams& params(GetParam());
+
+ Buffer subjectPublicKeyInfoBuffer(
+ HexToBytes(params.mSubjectPublicKeyInfoHex));
+ Input subjectPublicKeyInfoInput = InputForBuffer(subjectPublicKeyInfoBuffer);
+
+ Buffer signedTreeHeadBuffer(HexToBytes(params.mSignedTreeHeadHex));
+ Input signedTreeHeadInput = InputForBuffer(signedTreeHeadBuffer);
+
+ SignedTreeHeadDataV2 sth;
+ EXPECT_EQ(params.mExpectedSignedTreeHeadResult,
+ DecodeAndVerifySignedTreeHead(
+ subjectPublicKeyInfoInput, params.mDigestAlgorithm,
+ params.mPublicKeyAlgorithm, signedTreeHeadInput, sth));
+
+ if (params.mExpectedSignedTreeHeadResult == Success) {
+ EXPECT_EQ(params.mExpectedTimestamp, sth.timestamp);
+ EXPECT_EQ(params.mExpectedTreeSize, sth.treeSize);
+ EXPECT_EQ(HexToBytes(params.mExpectedRootHashHex), sth.rootHash);
+ }
+}
+
+INSTANTIATE_TEST_SUITE_P(BTSignedTreeHeadTest, BTSignedTreeHeadTest,
+ testing::ValuesIn(BT_SIGNED_TREE_HEAD_TEST_PARAMS));
+
+TEST_F(BTSignedTreeHeadTest, BTSignedTreeHeadTamperedSignatureTest) {
+ Buffer subjectPublicKeyInfoBuffer(HexToBytes(ValidSTH::kSPKIHex));
+ Input subjectPublicKeyInfoInput = InputForBuffer(subjectPublicKeyInfoBuffer);
+
+ Buffer signedTreeHeadBuffer(HexToBytes(ValidSTH::kSTHHex));
+ ASSERT_TRUE(signedTreeHeadBuffer.size() > 15);
+ signedTreeHeadBuffer[signedTreeHeadBuffer.size() - 15] ^= 0xff;
+ Input signedTreeHeadInput = InputForBuffer(signedTreeHeadBuffer);
+
+ SignedTreeHeadDataV2 sth;
+ EXPECT_EQ(Result::ERROR_BAD_SIGNATURE,
+ DecodeAndVerifySignedTreeHead(subjectPublicKeyInfoInput,
+ pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA,
+ signedTreeHeadInput, sth));
+}
+
+TEST_F(BTSignedTreeHeadTest, BTSignedTreeHeadTruncatedSignatureTest) {
+ Buffer subjectPublicKeyInfoBuffer(HexToBytes(ValidSTH::kSPKIHex));
+ Input subjectPublicKeyInfoInput = InputForBuffer(subjectPublicKeyInfoBuffer);
+
+ Buffer signedTreeHeadBuffer(HexToBytes(ValidSTH::kSTHHex));
+ ASSERT_TRUE(signedTreeHeadBuffer.size() > 17);
+ signedTreeHeadBuffer.resize(signedTreeHeadBuffer.size() - 17);
+ Input signedTreeHeadInput = InputForBuffer(signedTreeHeadBuffer);
+
+ SignedTreeHeadDataV2 sth;
+ EXPECT_EQ(Result::ERROR_BAD_DER,
+ DecodeAndVerifySignedTreeHead(subjectPublicKeyInfoInput,
+ pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA,
+ signedTreeHeadInput, sth));
+}
+
+TEST_F(BTSignedTreeHeadTest, BTSignedTreeHeadMissingSignatureTest) {
+ Buffer subjectPublicKeyInfoBuffer(HexToBytes(ValidSTH::kSPKIHex));
+ Input subjectPublicKeyInfoInput = InputForBuffer(subjectPublicKeyInfoBuffer);
+
+ Buffer signedTreeHeadBuffer = {
+ 0x02, 0x00, 0x00,
+ // 1541189938000 milliseconds since the epoch
+ 0x00, 0x00, 0x01, 0x66, 0xd6, 0x14, 0x2b, 0x50, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x07, // 7 total nodes
+ 0x20, // 32 byte hash
+ 0xd1, 0xa0, 0xd3, 0x94, 0x7d, 0xb4, 0xae, 0x83, 0x05, 0xf2, 0xac, 0x32,
+ 0x98, 0x59, 0x57, 0xe0, 0x26, 0x59, 0xb2, 0xea, 0x3c, 0x10, 0xda, 0x52,
+ 0xa4, 0x8d, 0x25, 0x26, 0xe9, 0xaf, 0x3b, 0xbc, 0x00,
+ 0x00, // no extensions
+ // missing signature
+ };
+ Input signedTreeHeadInput = InputForBuffer(signedTreeHeadBuffer);
+
+ SignedTreeHeadDataV2 sth;
+ EXPECT_EQ(Result::ERROR_BAD_DER,
+ DecodeAndVerifySignedTreeHead(subjectPublicKeyInfoInput,
+ pkix::DigestAlgorithm::sha256,
+ pkix::der::PublicKeyAlgorithm::ECDSA,
+ signedTreeHeadInput, sth));
+}
+
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/tests/gtest/BTVerificationTest.cpp b/security/ct/tests/gtest/BTVerificationTest.cpp
new file mode 100644
index 0000000000..41d8a11d2c
--- /dev/null
+++ b/security/ct/tests/gtest/BTVerificationTest.cpp
@@ -0,0 +1,257 @@
+/* -*- 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 "BTVerifier.h"
+#include "CTTestUtils.h"
+#include "gtest/gtest.h"
+
+#include "nss.h"
+
+namespace mozilla {
+namespace ct {
+
+using namespace pkix;
+
+struct BTVerificationTestParams {
+ const char* mInclusionProofHex;
+ const char* mExpectedRootHashHex;
+ const char* mInputHex;
+ pkix::DigestAlgorithm mDigestAlgorithm;
+ pkix::Result mExpectedVerificationResult;
+};
+
+class BTVerificationTest
+ : public ::testing::Test,
+ public ::testing::WithParamInterface<BTVerificationTestParams> {
+ void SetUp() override {
+ if (!NSS_IsInitialized()) {
+ if (NSS_NoDB_Init(nullptr) != SECSuccess) {
+ abort();
+ }
+ }
+ }
+};
+
+// This comes from testing/mozharness/test/test_mozilla_merkle.py
+static const char* kValidInclusionProofHex =
+ "020000"
+ "0000000000000007" // 7 total nodes
+ "0000000000000005" // leaf index is 5
+ "0063" // 99 (in base 10) bytes follow
+ "20" // 32 byte hash
+ "2ff3bdac9bec3580b82da8a357746f15919414d9cbe517e2dd96910c9814c30c"
+ "20"
+ "bb13dfb7b202a95f241ea1715c8549dc048d9936ec747028002f7c795de72fcf"
+ "20"
+ "380d0dc6fd7d4f37859a12dbfc7171b3cce29ab0688c6cffd2b15f3e0b21af49";
+
+static const char* kExpectedRootHashHex =
+ "d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc";
+
+static const char* kInputHex = "81aae91cf90be172eedd1c75c349bf9e";
+static const char* kAlteredInputHex = "81aae91c0000000000000000c349bf9e";
+
+// Same as kValidInclusionProofHex, but missing the last hash (the proof is
+// still structurally valid, so it decodes successfully).
+static const char* kShortInclusionProofHex =
+ "020000"
+ "0000000000000007" // 7 total nodes
+ "0000000000000005" // leaf index is 5
+ "0042" // 66 (in base 10) bytes follow
+ "20" // 32 byte hash
+ "2ff3bdac9bec3580b82da8a357746f15919414d9cbe517e2dd96910c9814c30c"
+ "20"
+ "bb13dfb7b202a95f241ea1715c8549dc048d9936ec747028002f7c795de72fcf";
+
+// Same as kValidInclusionProofHex, but has too many hashes (the proof is
+// still structurally valid, so it decodes successfully).
+static const char* kLongInclusionProofHex =
+ "020000"
+ "0000000000000007" // 7 total nodes
+ "0000000000000005" // leaf index is 5
+ "0084" // 132 (in base 10) bytes follow
+ "20" // 32 byte hash
+ "2ff3bdac9bec3580b82da8a357746f15919414d9cbe517e2dd96910c9814c30c"
+ "20"
+ "bb13dfb7b202a95f241ea1715c8549dc048d9936ec747028002f7c795de72fcf"
+ "20"
+ "380d0dc6fd7d4f37859a12dbfc7171b3cce29ab0688c6cffd2b15f3e0b21af49"
+ "20"
+ "abbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabbaabba";
+
+static const char* kIncorrectRootHashHex =
+ "beebeebeebeebeebeebeebeebeebeebeebeebeebeebeebeebeebeebeebeebeee";
+
+static const char* kWrongTreeSizeInclusionProofHex =
+ "020000"
+ "0000000000000009" // 9 total nodes (this is wrong)
+ "0000000000000005" // leaf index is 5
+ "0063" // 99 (in base 10) bytes follow
+ "20" // 32 byte hash
+ "2ff3bdac9bec3580b82da8a357746f15919414d9cbe517e2dd96910c9814c30c"
+ "20"
+ "bb13dfb7b202a95f241ea1715c8549dc048d9936ec747028002f7c795de72fcf"
+ "20"
+ "380d0dc6fd7d4f37859a12dbfc7171b3cce29ab0688c6cffd2b15f3e0b21af49";
+
+static const char* kWrongLeafIndexInclusionProofHex =
+ "020000"
+ "0000000000000007" // 7 total nodes
+ "0000000000000002" // leaf index is 2 (this is wrong)
+ "0063" // 99 (in base 10) bytes follow
+ "20" // 32 byte hash
+ "2ff3bdac9bec3580b82da8a357746f15919414d9cbe517e2dd96910c9814c30c"
+ "20"
+ "bb13dfb7b202a95f241ea1715c8549dc048d9936ec747028002f7c795de72fcf"
+ "20"
+ "380d0dc6fd7d4f37859a12dbfc7171b3cce29ab0688c6cffd2b15f3e0b21af49";
+
+// This is a longer proof taken from the Firefox 62.0.2 release.
+static const char* kValidInclusionProof2Hex =
+ "020000"
+ "0000000000000cb3"
+ "00000000000000e2"
+ "018c"
+ "20"
+ "6d3d060c72c28cd1967a6ce4e87a470c266981ee8beb6e5956745b5839120338"
+ "20"
+ "cd028053c51f52447cd2b1ffea7761be546b72832d776db155008dc3071231eb"
+ "20"
+ "36167b32f7ba4980257c79ed65d133d8c53efbfd7addaa391a121953c78b2b98"
+ "20"
+ "f2b4abf09140b05058a74e25f4008157655e73fa4aeece9b3f2a40dbbae95c16"
+ "20"
+ "a2be4813b2ca0e7bb755ed5459789c25a83465fb3f2808590c04c64cdbb404ef"
+ "20"
+ "43e4341b74ea11f075f35761b867cb256e9d42c4c1253d6b5c958174daa800da"
+ "20"
+ "1c925ec8d2d4014c0b84d7bed8065d9e7b326e3e4eaebc90d7a6a66431249851"
+ "20"
+ "bd275e9e4e48c3725f6ef68c2661d4d6ae415129cfb0ea0881177f5347d683ae"
+ "20"
+ "01e84996a7cc1719b19a4a7e48941657b125bcd0dfd03d183daff87de6a480c3"
+ "20"
+ "664b6962909692980661be55e8bdadc48350b2461d37372457e8ff97824ae8da"
+ "20"
+ "39f7ab1500a58643fccc9e07ed409be4c707c5536b95c57f6716207656464019"
+ "20"
+ "d68c856664d660456d4c85498c2cc2abfb955a63bd1fb23f3e25850da80153f6";
+
+static const char* kExpectedRootHash2Hex =
+ "0d489b16a64b80035a8d08ce55549c89c4b67be64a0456382cc5be1e51ddae1a";
+
+// This the hex encoding of the SHA-256 hash of
+// linux-x86_64/en-US/firefox-62.0.2.tar.bz2, encoded (again) in hex (because
+// that's how generate-checksums.py currently builds the binary transparency
+// tree).
+static const char* kInput2Hex =
+ "3231623461306361626538663030306261663236373235643530313462386434"
+ "3865353561333832336538653665656131343638313062313031623034396536";
+
+// This is the hex encoding of an inclusion proof for
+// update/win32/en-GB/firefox-61.0-62.0.2.partial.mar using SHA-512.
+static const char* kValidInclusionProofSHA512Hex =
+ "020000"
+ "0000000000000cb3"
+ "00000000000007ac"
+ "030c"
+ "40"
+ "b39fbe359108724ec1daed2a8c88511c6b94a74563bfaaf25b774286ab03cab9"
+ "b2e64bbd4eeb02003b7397cd814b5d18a160e14ad08eca04559abe146201e027"
+ "40"
+ "de119745a41884ed482a41638cc1979d89e1bd0ca295e0c412e7ee4b5c2e042d"
+ "a1f4aa9c7f1e58b78fac10400b207197383177401976ef6baa80d482c102a94f"
+ "40"
+ "d0fe203b0e95cbe48e2e663538aba382718c85ae167d8c4ccf326519f81b3860"
+ "55058f6a49943b8ed228af88b3414ec00e939ad8dc736f502ca90d63494eddcd"
+ "40"
+ "ae825352bbe12845fb1d3be27375c27dd462ebd6dc6d7bd0b5929df97aaccf1a"
+ "9eedb304ae1e9742b1e2a2ba8b22408d2d52e3bb3d547632d5f03cfe112572d2"
+ "40"
+ "1ab0f7f7d674c691cf8e51d4409d3402d94927231b82f7f71f16828dd9e36152"
+ "1a69f7e571df06aea16c83df88dc74042444ae8771375a7118477275395b570d"
+ "40"
+ "9c6b94c5be456c68518753cf626daf11b30ce981e18b76c036664dcafeba19b7"
+ "3f5ee20ab35aa38b11c92e2c8f103d1b19113b7171c615ce0b9c5ff520242d8c"
+ "40"
+ "d395ef3cd63ae617a96bd6c5c361d5f6aaccff1ed90599115394310dfa0c528d"
+ "611998d325f374c01a827ee900d72ed66d299845438bba565ba666c588b232a0"
+ "40"
+ "ce3548561f5ec7a94a6c7a2adc604f5e32869d0727cee80f12c95cab2a996965"
+ "6ad61a02b35e0fb2d0031ea496381f1413d33b8cbfdf092accd903881e083ee0"
+ "40"
+ "a4247b649fee9017d9c9e729c4763719b7155056013abc488de0079dc7bddce6"
+ "57a93f2596967cfb123d812a901bc4ffe0338cbecf30adc1a821e7261f88e3cd"
+ "40"
+ "7d2056ea6645b0306373fa2e26fbc4a4ccd4c82cb7d605d2372fa297654cd3a0"
+ "b151f6b0fffe7472661de79c7ce7873cb75e05cad5ea40173be540e16d51414d"
+ "40"
+ "eed2c98b3d8b8035777af74a0d5dd38963f84c09e6f42b6ca4e290674c175035"
+ "b9bc85bb4304fcf3d042c444f951b3b134563c6936a1b176679e35e94c42416b"
+ "40"
+ "6167cccd9d03909f95bb389936766340234b5b3e35dfa8c81d330db9055d7b7d"
+ "bfaf262f1821b4a68f43af62c53987550b4174eaab8357f9bd83f2715c98d441";
+
+// This is the hex encoding of the expected root hash of the SHA-512 tree.
+static const char* kExpectedRootHashSHA512Hex =
+ "6f20812da2980393767e98dd7f79d89aa41e029c790d3908739c02c01d9c6525"
+ "8bb3e24ab608c95b063259620751ce258930d48929cbe543b166c33c4de65e60";
+
+// This the hex encoding of the SHA-512 hash of
+// update/win32/en-GB/firefox-61.0-62.0.2.partial.mar, encoded (again) in hex.
+static const char* kInputSHA512Hex =
+ "6237656563626164353363386432346230383763666437323333303162326437"
+ "6234326136323536663462636336356134623231636437376133613830613734"
+ "3865396533623666306432333839666234623763396563356638643833373534"
+ "6239313635313165333661373238653863303236376564653761666561316638";
+
+static const BTVerificationTestParams BT_VERIFICATION_TEST_PARAMS[] = {
+ {kValidInclusionProofHex, kExpectedRootHashHex, kInputHex,
+ pkix::DigestAlgorithm::sha256, Success},
+ {kValidInclusionProofHex, kExpectedRootHashHex, kAlteredInputHex,
+ pkix::DigestAlgorithm::sha256, pkix::Result::ERROR_BAD_SIGNATURE},
+ {kShortInclusionProofHex, kExpectedRootHashHex, kInputHex,
+ pkix::DigestAlgorithm::sha256, pkix::Result::ERROR_BAD_SIGNATURE},
+ {kLongInclusionProofHex, kExpectedRootHashHex, kInputHex,
+ pkix::DigestAlgorithm::sha256, pkix::Result::ERROR_BAD_SIGNATURE},
+ {kValidInclusionProofHex, kIncorrectRootHashHex, kInputHex,
+ pkix::DigestAlgorithm::sha256, pkix::Result::ERROR_BAD_SIGNATURE},
+ {kWrongTreeSizeInclusionProofHex, kExpectedRootHashHex, kInputHex,
+ pkix::DigestAlgorithm::sha256, pkix::Result::ERROR_BAD_SIGNATURE},
+ {kWrongLeafIndexInclusionProofHex, kExpectedRootHashHex, kInputHex,
+ pkix::DigestAlgorithm::sha256, pkix::Result::ERROR_BAD_SIGNATURE},
+ {kValidInclusionProofHex, kExpectedRootHashHex, kInputHex,
+ pkix::DigestAlgorithm::sha512, pkix::Result::ERROR_BAD_SIGNATURE},
+ {kValidInclusionProof2Hex, kExpectedRootHash2Hex, kInput2Hex,
+ pkix::DigestAlgorithm::sha256, Success},
+ {kValidInclusionProofSHA512Hex, kExpectedRootHashSHA512Hex, kInputSHA512Hex,
+ pkix::DigestAlgorithm::sha512, Success},
+ {kValidInclusionProofSHA512Hex, kExpectedRootHashSHA512Hex, kInputSHA512Hex,
+ pkix::DigestAlgorithm::sha256, pkix::Result::ERROR_BAD_SIGNATURE},
+};
+
+TEST_P(BTVerificationTest, BTVerificationSimpleTest) {
+ const BTVerificationTestParams& params(GetParam());
+
+ Buffer encodedProofBuffer(HexToBytes(params.mInclusionProofHex));
+ Input encodedProof = InputForBuffer(encodedProofBuffer);
+ InclusionProofDataV2 ipr;
+ ASSERT_EQ(Success, DecodeInclusionProof(encodedProof, ipr));
+
+ Buffer leafEntryBuffer(HexToBytes(params.mInputHex));
+ Input leafEntry = InputForBuffer(leafEntryBuffer);
+ Buffer expectedRootHashBuffer(HexToBytes(params.mExpectedRootHashHex));
+ Input expectedRootHash = InputForBuffer(expectedRootHashBuffer);
+ ASSERT_EQ(params.mExpectedVerificationResult,
+ VerifyInclusionProof(ipr, leafEntry, expectedRootHash,
+ params.mDigestAlgorithm));
+}
+
+INSTANTIATE_TEST_SUITE_P(BTVerificationTest, BTVerificationTest,
+ testing::ValuesIn(BT_VERIFICATION_TEST_PARAMS));
+
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/tests/gtest/CTDiversityPolicyTest.cpp b/security/ct/tests/gtest/CTDiversityPolicyTest.cpp
new file mode 100644
index 0000000000..bf999acea4
--- /dev/null
+++ b/security/ct/tests/gtest/CTDiversityPolicyTest.cpp
@@ -0,0 +1,15 @@
+/* -*- 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 "CTDiversityPolicy.h"
+
+namespace mozilla {
+namespace ct {
+
+// TBD, PENDING FINALIZATION OF MOZILLA CT POLICY.
+
+}
+} // namespace mozilla
diff --git a/security/ct/tests/gtest/CTLogVerifierTest.cpp b/security/ct/tests/gtest/CTLogVerifierTest.cpp
new file mode 100644
index 0000000000..1b7374a836
--- /dev/null
+++ b/security/ct/tests/gtest/CTLogVerifierTest.cpp
@@ -0,0 +1,118 @@
+/* -*- 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 "CTTestUtils.h"
+#include "nss.h"
+
+#include "gtest/gtest.h"
+
+namespace mozilla {
+namespace ct {
+
+using namespace pkix;
+
+class CTLogVerifierTest : public ::testing::Test {
+ public:
+ void SetUp() override {
+ // Does nothing if NSS is already initialized.
+ if (NSS_NoDB_Init(nullptr) != SECSuccess) {
+ abort();
+ }
+
+ 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) {
+ LogEntry certEntry;
+ GetX509CertLogEntry(certEntry);
+
+ SignedCertificateTimestamp certSct;
+ GetX509CertSCT(certSct);
+
+ EXPECT_EQ(Success, mLog.Verify(certEntry, certSct));
+}
+
+TEST_F(CTLogVerifierTest, VerifiesPrecertSCT) {
+ LogEntry precertEntry;
+ GetPrecertLogEntry(precertEntry);
+
+ SignedCertificateTimestamp precertSct;
+ GetPrecertSCT(precertSct);
+
+ EXPECT_EQ(Success, mLog.Verify(precertEntry, precertSct));
+}
+
+TEST_F(CTLogVerifierTest, FailsInvalidTimestamp) {
+ LogEntry certEntry;
+ GetX509CertLogEntry(certEntry);
+
+ SignedCertificateTimestamp certSct;
+ GetX509CertSCT(certSct);
+
+ // Mangle the timestamp, so that it should fail signature validation.
+ certSct.timestamp = 0;
+
+ EXPECT_EQ(pkix::Result::ERROR_BAD_SIGNATURE, mLog.Verify(certEntry, certSct));
+}
+
+TEST_F(CTLogVerifierTest, FailsInvalidSignature) {
+ LogEntry certEntry;
+ GetX509CertLogEntry(certEntry);
+
+ // Mangle the value of the signature, making the underlying signature
+ // verification code return ERROR_BAD_SIGNATURE.
+ SignedCertificateTimestamp certSct;
+ GetX509CertSCT(certSct);
+ certSct.signature.signatureData[20] ^= '\xFF';
+ EXPECT_EQ(pkix::Result::ERROR_BAD_SIGNATURE, mLog.Verify(certEntry, certSct));
+
+ // Mangle the encoding of the signature, making the underlying implementation
+ // return ERROR_BAD_DER. We still expect the verifier to return
+ // ERROR_BAD_SIGNATURE.
+ SignedCertificateTimestamp certSct2;
+ GetX509CertSCT(certSct2);
+ certSct2.signature.signatureData[0] ^= '\xFF';
+ EXPECT_EQ(pkix::Result::ERROR_BAD_SIGNATURE,
+ mLog.Verify(certEntry, certSct2));
+}
+
+TEST_F(CTLogVerifierTest, FailsInvalidLogID) {
+ LogEntry certEntry;
+ GetX509CertLogEntry(certEntry);
+
+ SignedCertificateTimestamp certSct;
+ GetX509CertSCT(certSct);
+
+ // Mangle the log ID, which should cause it to match a different log before
+ // attempting signature validation.
+ certSct.logId.push_back('\x0');
+
+ EXPECT_EQ(pkix::Result::FATAL_ERROR_INVALID_ARGS,
+ mLog.Verify(certEntry, certSct));
+}
+
+// Test that excess data after the public key is rejected.
+TEST_F(CTLogVerifierTest, ExcessDataInPublicKey) {
+ Buffer key = GetTestPublicKey();
+ std::string extra = "extra";
+ key.insert(key.end(), extra.begin(), extra.end());
+
+ CTLogVerifier log;
+ EXPECT_NE(Success,
+ log.Init(InputForBuffer(key), -1 /*operator id*/,
+ CTLogStatus::Included, 0 /*disqualification time*/));
+}
+
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/tests/gtest/CTObjectsExtractorTest.cpp b/security/ct/tests/gtest/CTObjectsExtractorTest.cpp
new file mode 100644
index 0000000000..20b454b98c
--- /dev/null
+++ b/security/ct/tests/gtest/CTObjectsExtractorTest.cpp
@@ -0,0 +1,82 @@
+/* -*- 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 "CTObjectsExtractor.h"
+#include "CTSerialization.h"
+#include "CTTestUtils.h"
+#include "gtest/gtest.h"
+#include "nss.h"
+
+namespace mozilla {
+namespace ct {
+
+using namespace pkix;
+
+class CTObjectsExtractorTest : public ::testing::Test {
+ public:
+ void SetUp() override {
+ // Does nothing if NSS is already initialized.
+ if (NSS_NoDB_Init(nullptr) != SECSuccess) {
+ abort();
+ }
+
+ mTestCert = GetDEREncodedX509Cert();
+ mEmbeddedCert = GetDEREncodedTestEmbeddedCert();
+ mCaCert = GetDEREncodedCACert();
+ mCaCertSPKI = ExtractCertSPKI(mCaCert);
+
+ Buffer logPublicKey = GetTestPublicKey();
+ 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;
+};
+
+TEST_F(CTObjectsExtractorTest, ExtractPrecert) {
+ LogEntry entry;
+ ASSERT_EQ(Success, GetPrecertLogEntry(InputForBuffer(mEmbeddedCert),
+ InputForBuffer(mCaCertSPKI), entry));
+
+ EXPECT_EQ(LogEntry::Type::Precert, entry.type);
+ // Should have empty leaf cert for this log entry type.
+ EXPECT_TRUE(entry.leafCertificate.empty());
+ EXPECT_EQ(GetDefaultIssuerKeyHash(), entry.issuerKeyHash);
+ EXPECT_EQ(GetDEREncodedTestTbsCert(), entry.tbsCertificate);
+}
+
+TEST_F(CTObjectsExtractorTest, ExtractOrdinaryX509Cert) {
+ LogEntry entry;
+ GetX509LogEntry(InputForBuffer(mTestCert), entry);
+
+ EXPECT_EQ(LogEntry::Type::X509, entry.type);
+ // Should have empty tbsCertificate / issuerKeyHash for this log entry type.
+ EXPECT_TRUE(entry.tbsCertificate.empty());
+ EXPECT_TRUE(entry.issuerKeyHash.empty());
+ // Length of leafCertificate should be 718, see the CT Serialization tests.
+ EXPECT_EQ(718U, entry.leafCertificate.size());
+}
+
+// Test that an externally-provided SCT verifies over the LogEntry
+// of a regular X.509 Certificate
+TEST_F(CTObjectsExtractorTest, ComplementarySCTVerifies) {
+ SignedCertificateTimestamp sct;
+ GetX509CertSCT(sct);
+
+ LogEntry entry;
+ GetX509LogEntry(InputForBuffer(mTestCert), entry);
+ EXPECT_EQ(Success, mLog.Verify(entry, sct));
+}
+
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/tests/gtest/CTPolicyEnforcerTest.cpp b/security/ct/tests/gtest/CTPolicyEnforcerTest.cpp
new file mode 100644
index 0000000000..7bc5b296a5
--- /dev/null
+++ b/security/ct/tests/gtest/CTPolicyEnforcerTest.cpp
@@ -0,0 +1,387 @@
+/* -*- 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 "CTPolicyEnforcer.h"
+
+#include <algorithm>
+#include <stdint.h>
+#include <stdio.h>
+
+#include "CTLogVerifier.h"
+#include "CTVerifyResult.h"
+#include "SignedCertificateTimestamp.h"
+#include "mozpkix/Time.h"
+#include "gtest/gtest.h"
+#include "hasht.h"
+#include "prtime.h"
+
+// Implemented in CertVerifier.cpp.
+extern mozilla::pkix::Result GetCertLifetimeInFullMonths(
+ mozilla::pkix::Time certNotBefore, mozilla::pkix::Time certNotAfter,
+ size_t& months);
+
+namespace mozilla {
+namespace ct {
+
+using namespace mozilla::pkix;
+
+class CTPolicyEnforcerTest : public ::testing::Test {
+ public:
+ void SetUp() override {
+ OPERATORS_1_AND_2.push_back(OPERATOR_1);
+ OPERATORS_1_AND_2.push_back(OPERATOR_2);
+ }
+
+ void GetLogId(Buffer& logId, size_t logNo) {
+ logId.resize(SHA256_LENGTH);
+ std::fill(logId.begin(), logId.end(), 0);
+ // Just raw-copy |logId| into the output buffer.
+ assert(sizeof(logNo) <= logId.size());
+ memcpy(logId.data(), &logNo, sizeof(logNo));
+ }
+
+ void AddSct(VerifiedSCTList& verifiedScts, size_t logNo,
+ CTLogOperatorId operatorId, VerifiedSCT::Origin origin,
+ uint64_t timestamp,
+ VerifiedSCT::Status status = VerifiedSCT::Status::Valid) {
+ VerifiedSCT verifiedSct;
+ verifiedSct.status = status;
+ verifiedSct.origin = origin;
+ verifiedSct.logOperatorId = operatorId;
+ verifiedSct.logDisqualificationTime =
+ status == VerifiedSCT::Status::ValidFromDisqualifiedLog
+ ? DISQUALIFIED_AT
+ : UINT64_MAX;
+ verifiedSct.sct.version = SignedCertificateTimestamp::Version::V1;
+ verifiedSct.sct.timestamp = timestamp;
+ Buffer logId;
+ GetLogId(logId, logNo);
+ verifiedSct.sct.logId = std::move(logId);
+ verifiedScts.push_back(std::move(verifiedSct));
+ }
+
+ void AddMultipleScts(
+ VerifiedSCTList& verifiedScts, size_t logsCount, uint8_t operatorsCount,
+ VerifiedSCT::Origin origin, uint64_t timestamp,
+ VerifiedSCT::Status status = VerifiedSCT::Status::Valid) {
+ for (size_t logNo = 0; logNo < logsCount; logNo++) {
+ CTLogOperatorId operatorId = logNo % operatorsCount;
+ AddSct(verifiedScts, logNo, operatorId, origin, timestamp, status);
+ }
+ }
+
+ void CheckCompliance(const VerifiedSCTList& verifiedSct,
+ size_t certLifetimeInCalendarMonths,
+ const CTLogOperatorList& dependentLogOperators,
+ CTPolicyCompliance expectedCompliance) {
+ CTPolicyCompliance compliance;
+ mPolicyEnforcer.CheckCompliance(verifiedSct, certLifetimeInCalendarMonths,
+ dependentLogOperators, compliance);
+ EXPECT_EQ(expectedCompliance, compliance);
+ }
+
+ protected:
+ CTPolicyEnforcer mPolicyEnforcer;
+
+ const size_t LOG_1 = 1;
+ const size_t LOG_2 = 2;
+ const size_t LOG_3 = 3;
+ const size_t LOG_4 = 4;
+ const size_t LOG_5 = 5;
+
+ const CTLogOperatorId OPERATOR_1 = 1;
+ const CTLogOperatorId OPERATOR_2 = 2;
+ const CTLogOperatorId OPERATOR_3 = 3;
+
+ CTLogOperatorList NO_OPERATORS;
+ CTLogOperatorList OPERATORS_1_AND_2;
+
+ const VerifiedSCT::Origin ORIGIN_EMBEDDED = VerifiedSCT::Origin::Embedded;
+ const VerifiedSCT::Origin ORIGIN_TLS = VerifiedSCT::Origin::TLSExtension;
+ const VerifiedSCT::Origin ORIGIN_OCSP = VerifiedSCT::Origin::OCSPResponse;
+
+ // 4 years of cert lifetime requires 5 SCTs for the embedded case.
+ const size_t DEFAULT_MONTHS = 4 * 12L;
+
+ // Date.parse("2015-08-15T00:00:00Z")
+ const uint64_t TIMESTAMP_1 = 1439596800000L;
+
+ // Date.parse("2016-04-15T00:00:00Z")
+ const uint64_t DISQUALIFIED_AT = 1460678400000L;
+
+ // Date.parse("2016-04-01T00:00:00Z")
+ const uint64_t BEFORE_DISQUALIFIED = 1459468800000L;
+
+ // Date.parse("2016-04-16T00:00:00Z")
+ const uint64_t AFTER_DISQUALIFIED = 1460764800000L;
+};
+
+TEST_F(CTPolicyEnforcerTest, ConformsToCTPolicyWithNonEmbeddedSCTs) {
+ VerifiedSCTList scts;
+
+ AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1);
+ AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, TIMESTAMP_1);
+
+ CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+ CTPolicyCompliance::Compliant);
+}
+
+TEST_F(CTPolicyEnforcerTest, DoesNotConformNotEnoughDiverseNonEmbeddedSCTs) {
+ VerifiedSCTList scts;
+
+ AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1);
+ AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, TIMESTAMP_1);
+
+ CheckCompliance(scts, DEFAULT_MONTHS, OPERATORS_1_AND_2,
+ CTPolicyCompliance::NotDiverseScts);
+}
+
+TEST_F(CTPolicyEnforcerTest, ConformsToCTPolicyWithEmbeddedSCTs) {
+ VerifiedSCTList scts;
+
+ // 5 embedded SCTs required for DEFAULT_MONTHS.
+ AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_3, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_4, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_5, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1);
+
+ CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+ CTPolicyCompliance::Compliant);
+}
+
+TEST_F(CTPolicyEnforcerTest, DoesNotConformNotEnoughDiverseEmbeddedSCTs) {
+ VerifiedSCTList scts;
+
+ // 5 embedded SCTs required for DEFAULT_MONTHS.
+ AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_3, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_4, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_5, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1);
+
+ CheckCompliance(scts, DEFAULT_MONTHS, OPERATORS_1_AND_2,
+ CTPolicyCompliance::NotDiverseScts);
+}
+
+TEST_F(CTPolicyEnforcerTest, ConformsToCTPolicyWithPooledNonEmbeddedSCTs) {
+ VerifiedSCTList scts;
+
+ AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_OCSP, TIMESTAMP_1);
+ AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, TIMESTAMP_1);
+
+ CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+ CTPolicyCompliance::Compliant);
+}
+
+TEST_F(CTPolicyEnforcerTest, ConformsToCTPolicyWithPooledEmbeddedSCTs) {
+ VerifiedSCTList scts;
+
+ AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_OCSP, TIMESTAMP_1);
+
+ CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+ CTPolicyCompliance::Compliant);
+}
+
+TEST_F(CTPolicyEnforcerTest, DoesNotConformToCTPolicyNotEnoughSCTs) {
+ VerifiedSCTList scts;
+
+ AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1);
+
+ CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+ CTPolicyCompliance::NotEnoughScts);
+}
+
+TEST_F(CTPolicyEnforcerTest, DoesNotConformToCTPolicyNotEnoughFreshSCTs) {
+ VerifiedSCTList scts;
+
+ // The results should be the same before and after disqualification,
+ // regardless of the delivery method.
+
+ // SCT from before disqualification.
+ scts.clear();
+ AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1);
+ AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, BEFORE_DISQUALIFIED,
+ VerifiedSCT::Status::ValidFromDisqualifiedLog);
+ CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+ CTPolicyCompliance::NotEnoughScts);
+ // SCT from after disqualification.
+ scts.clear();
+ AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1);
+ AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_TLS, AFTER_DISQUALIFIED,
+ VerifiedSCT::Status::ValidFromDisqualifiedLog);
+ CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+ CTPolicyCompliance::NotEnoughScts);
+
+ // Embedded SCT from before disqualification.
+ scts.clear();
+ AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1);
+ AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_EMBEDDED, BEFORE_DISQUALIFIED,
+ VerifiedSCT::Status::ValidFromDisqualifiedLog);
+ CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+ CTPolicyCompliance::NotEnoughScts);
+
+ // Embedded SCT from after disqualification.
+ scts.clear();
+ AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_TLS, TIMESTAMP_1);
+ AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED,
+ VerifiedSCT::Status::ValidFromDisqualifiedLog);
+ CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+ CTPolicyCompliance::NotEnoughScts);
+}
+
+TEST_F(CTPolicyEnforcerTest,
+ ConformsWithDisqualifiedLogBeforeDisqualificationDate) {
+ VerifiedSCTList scts;
+
+ // 5 embedded SCTs required for DEFAULT_MONTHS.
+ AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_3, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_4, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_5, OPERATOR_2, ORIGIN_EMBEDDED, BEFORE_DISQUALIFIED,
+ VerifiedSCT::Status::ValidFromDisqualifiedLog);
+
+ CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+ CTPolicyCompliance::Compliant);
+}
+
+TEST_F(CTPolicyEnforcerTest,
+ DoesNotConformWithDisqualifiedLogAfterDisqualificationDate) {
+ VerifiedSCTList scts;
+
+ // 5 embedded SCTs required for DEFAULT_MONTHS.
+ AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_3, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_4, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_5, OPERATOR_2, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED,
+ VerifiedSCT::Status::ValidFromDisqualifiedLog);
+
+ CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+ CTPolicyCompliance::NotEnoughScts);
+}
+
+TEST_F(CTPolicyEnforcerTest,
+ DoesNotConformWithIssuanceDateAfterDisqualificationDate) {
+ VerifiedSCTList scts;
+
+ // 5 embedded SCTs required for DEFAULT_MONTHS.
+ AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED,
+ VerifiedSCT::Status::ValidFromDisqualifiedLog);
+ AddSct(scts, LOG_2, OPERATOR_1, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED);
+ AddSct(scts, LOG_3, OPERATOR_1, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED);
+ AddSct(scts, LOG_4, OPERATOR_1, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED);
+ AddSct(scts, LOG_5, OPERATOR_2, ORIGIN_EMBEDDED, AFTER_DISQUALIFIED);
+
+ CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+ CTPolicyCompliance::NotEnoughScts);
+}
+
+TEST_F(CTPolicyEnforcerTest,
+ DoesNotConformToCTPolicyNotEnoughUniqueEmbeddedLogs) {
+ VerifiedSCTList scts;
+
+ // Operator #1
+ AddSct(scts, LOG_1, OPERATOR_1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ // Operator #2, different logs
+ AddSct(scts, LOG_2, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_3, OPERATOR_2, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ // Operator #3, same log
+ AddSct(scts, LOG_4, OPERATOR_3, ORIGIN_EMBEDDED, TIMESTAMP_1);
+ AddSct(scts, LOG_4, OPERATOR_3, ORIGIN_EMBEDDED, TIMESTAMP_1);
+
+ // 5 embedded SCTs required. However, only 4 are from distinct logs.
+ CheckCompliance(scts, DEFAULT_MONTHS, NO_OPERATORS,
+ CTPolicyCompliance::NotEnoughScts);
+}
+
+TEST_F(CTPolicyEnforcerTest,
+ ConformsToPolicyExactNumberOfSCTsForValidityPeriod) {
+ // Test multiple validity periods.
+ const struct TestData {
+ size_t certLifetimeInCalendarMonths;
+ size_t sctsRequired;
+ } kTestData[] = {{3, 2}, {12 + 2, 2}, {12 + 3, 3},
+ {2 * 12 + 2, 3}, {2 * 12 + 3, 4}, {3 * 12 + 2, 4},
+ {3 * 12 + 4, 5}};
+
+ for (size_t i = 0; i < MOZILLA_CT_ARRAY_LENGTH(kTestData); ++i) {
+ SCOPED_TRACE(i);
+
+ size_t months = kTestData[i].certLifetimeInCalendarMonths;
+ size_t sctsRequired = kTestData[i].sctsRequired;
+
+ // Less SCTs than required is not enough.
+ for (size_t sctsAvailable = 0; sctsAvailable < sctsRequired;
+ ++sctsAvailable) {
+ VerifiedSCTList scts;
+ AddMultipleScts(scts, sctsAvailable, 1, ORIGIN_EMBEDDED, TIMESTAMP_1);
+
+ CTPolicyCompliance compliance;
+ mPolicyEnforcer.CheckCompliance(scts, months, NO_OPERATORS, compliance);
+ EXPECT_EQ(CTPolicyCompliance::NotEnoughScts, compliance)
+ << "i=" << i << " sctsRequired=" << sctsRequired
+ << " sctsAvailable=" << sctsAvailable;
+ }
+
+ // Add exactly the required number of SCTs (from 2 operators).
+ VerifiedSCTList scts;
+ AddMultipleScts(scts, sctsRequired, 2, ORIGIN_EMBEDDED, TIMESTAMP_1);
+
+ CTPolicyCompliance compliance;
+ mPolicyEnforcer.CheckCompliance(scts, months, NO_OPERATORS, compliance);
+ EXPECT_EQ(CTPolicyCompliance::Compliant, compliance) << "i=" << i;
+ }
+}
+
+TEST_F(CTPolicyEnforcerTest, TestEdgeCasesOfGetCertLifetimeInFullMonths) {
+ const struct TestData {
+ uint64_t notBefore;
+ uint64_t notAfter;
+ size_t expectedMonths;
+ } kTestData[] = {
+ { // 1 second less than 1 month
+ 1424863500000000, // Date.parse("2015-02-25T11:25:00Z") * 1000
+ 1427196299000000, // Date.parse("2015-03-24T11:24:59Z") * 1000
+ 0},
+ { // exactly 1 month
+ 1424863500000000, // Date.parse("2015-02-25T11:25:00Z") * 1000
+ 1427282700000000, // Date.parse("2015-03-25T11:25:00Z") * 1000
+ 1},
+ { // 1 year, 1 month
+ 1427282700000000, // Date.parse("2015-03-25T11:25:00Z") * 1000
+ 1461583500000000, // Date.parse("2016-04-25T11:25:00Z") * 1000
+ 13},
+ {// 1 year, 1 month, first day of notBefore month, last of notAfter
+ 1425209100000000, // Date.parse("2015-03-01T11:25:00Z") * 1000
+ 1462015500000000, // Date.parse("2016-04-30T11:25:00Z") * 1000
+ 13},
+ {// 1 year, adjacent months, last day of notBefore month, first of
+ // notAfter
+ 1427801100000000, // Date.parse("2015-03-31T11:25:00Z") * 1000
+ 1459509900000000, // Date.parse("2016-04-01T11:25:00Z") * 1000
+ 12}};
+
+ for (size_t i = 0; i < MOZILLA_CT_ARRAY_LENGTH(kTestData); ++i) {
+ SCOPED_TRACE(i);
+
+ size_t months;
+ ASSERT_EQ(Success,
+ GetCertLifetimeInFullMonths(mozilla::pkix::TimeFromEpochInSeconds(
+ kTestData[i].notBefore / 1000000),
+ mozilla::pkix::TimeFromEpochInSeconds(
+ kTestData[i].notAfter / 1000000),
+ months))
+ << "i=" << i;
+ EXPECT_EQ(kTestData[i].expectedMonths, months) << "i=" << i;
+ }
+}
+
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/tests/gtest/CTSerializationTest.cpp b/security/ct/tests/gtest/CTSerializationTest.cpp
new file mode 100644
index 0000000000..983759cf71
--- /dev/null
+++ b/security/ct/tests/gtest/CTSerializationTest.cpp
@@ -0,0 +1,216 @@
+/* -*- 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 "CTSerialization.h"
+#include "CTTestUtils.h"
+#include "gtest/gtest.h"
+
+namespace mozilla {
+namespace ct {
+
+using namespace pkix;
+
+class CTSerializationTest : public ::testing::Test {
+ public:
+ void SetUp() override {
+ mTestDigitallySigned = GetTestDigitallySigned();
+ mTestSignatureData = GetTestDigitallySignedData();
+ }
+
+ protected:
+ Buffer mTestDigitallySigned;
+ Buffer mTestSignatureData;
+};
+
+TEST_F(CTSerializationTest, DecodesDigitallySigned) {
+ Input digitallySigned = InputForBuffer(mTestDigitallySigned);
+ Reader digitallySignedReader(digitallySigned);
+
+ DigitallySigned parsed;
+ ASSERT_EQ(Success, DecodeDigitallySigned(digitallySignedReader, parsed));
+ EXPECT_TRUE(digitallySignedReader.AtEnd());
+
+ EXPECT_EQ(DigitallySigned::HashAlgorithm::SHA256, parsed.hashAlgorithm);
+ EXPECT_EQ(DigitallySigned::SignatureAlgorithm::ECDSA,
+ parsed.signatureAlgorithm);
+ EXPECT_EQ(mTestSignatureData, parsed.signatureData);
+}
+
+TEST_F(CTSerializationTest, FailsToDecodePartialDigitallySigned) {
+ Input partial;
+ ASSERT_EQ(Success, partial.Init(mTestDigitallySigned.data(),
+ mTestDigitallySigned.size() - 5));
+ Reader partialReader(partial);
+
+ DigitallySigned parsed;
+
+ EXPECT_NE(Success, DecodeDigitallySigned(partialReader, parsed));
+}
+
+TEST_F(CTSerializationTest, EncodesDigitallySigned) {
+ DigitallySigned digitallySigned;
+ digitallySigned.hashAlgorithm = DigitallySigned::HashAlgorithm::SHA256;
+ digitallySigned.signatureAlgorithm =
+ DigitallySigned::SignatureAlgorithm::ECDSA;
+ digitallySigned.signatureData = mTestSignatureData;
+
+ Buffer encoded;
+
+ ASSERT_EQ(Success, EncodeDigitallySigned(digitallySigned, encoded));
+ EXPECT_EQ(mTestDigitallySigned, encoded);
+}
+
+TEST_F(CTSerializationTest, EncodesLogEntryForX509Cert) {
+ LogEntry entry;
+ GetX509CertLogEntry(entry);
+
+ Buffer encoded;
+ ASSERT_EQ(Success, EncodeLogEntry(entry, encoded));
+ EXPECT_EQ((718U + 5U), encoded.size());
+ // First two bytes are log entry type. Next, length:
+ // Length is 718 which is 512 + 206, which is { 0, ..., 2, 206 }.
+ Buffer expectedPrefix = {0, 0, 0, 2, 206};
+ Buffer encodedPrefix;
+ encodedPrefix.assign(encoded.begin(), encoded.begin() + 5);
+ EXPECT_EQ(expectedPrefix, encodedPrefix);
+}
+
+TEST_F(CTSerializationTest, EncodesLogEntryForPrecert) {
+ LogEntry entry;
+ GetPrecertLogEntry(entry);
+
+ Buffer encoded;
+ ASSERT_EQ(Success, EncodeLogEntry(entry, encoded));
+ // log entry type + issuer key + length + tbsCertificate
+ EXPECT_EQ((2U + 32U + 3U + entry.tbsCertificate.size()), encoded.size());
+
+ // First two bytes are log entry type.
+ Buffer expectedPrefix = {0, 1};
+ Buffer encodedPrefix;
+ encodedPrefix.assign(encoded.begin(), encoded.begin() + 2);
+ EXPECT_EQ(expectedPrefix, encodedPrefix);
+
+ // Next is the issuer key (32 bytes).
+ Buffer encodedKeyHash;
+ encodedKeyHash.assign(encoded.begin() + 2, encoded.begin() + 2 + 32);
+ EXPECT_EQ(GetDefaultIssuerKeyHash(), encodedKeyHash);
+}
+
+TEST_F(CTSerializationTest, EncodesV1SCTSignedData) {
+ uint64_t timestamp = UINT64_C(0x139fe353cf5);
+ const uint8_t DUMMY_BYTES[] = {0x61, 0x62, 0x63}; // abc
+ Input dummyEntry(DUMMY_BYTES);
+ Input emptyExtensions;
+ Buffer encoded;
+ ASSERT_EQ(Success, EncodeV1SCTSignedData(timestamp, dummyEntry,
+ emptyExtensions, encoded));
+ EXPECT_EQ((size_t)15, encoded.size());
+
+ Buffer expectedBuffer = {
+ 0x00, // version
+ 0x00, // signature type
+ 0x00, 0x00, 0x01, 0x39, 0xFE, 0x35, 0x3C, 0xF5, // timestamp
+ 0x61, 0x62, 0x63, // log signature
+ 0x00, 0x00 // extensions (empty)
+ };
+ EXPECT_EQ(expectedBuffer, encoded);
+}
+
+TEST_F(CTSerializationTest, DecodesSCTList) {
+ // Two items in the list: "abc", "def"
+ const uint8_t ENCODED[] = {0x00, 0x0a, 0x00, 0x03, 0x61, 0x62,
+ 0x63, 0x00, 0x03, 0x64, 0x65, 0x66};
+ const uint8_t DECODED_1[] = {0x61, 0x62, 0x63};
+ const uint8_t DECODED_2[] = {0x64, 0x65, 0x66};
+
+ Reader listReader;
+ ASSERT_EQ(Success, DecodeSCTList(Input(ENCODED), listReader));
+
+ Input decoded1;
+ ASSERT_EQ(Success, ReadSCTListItem(listReader, decoded1));
+
+ Input decoded2;
+ ASSERT_EQ(Success, ReadSCTListItem(listReader, decoded2));
+
+ EXPECT_TRUE(listReader.AtEnd());
+ EXPECT_TRUE(InputsAreEqual(decoded1, Input(DECODED_1)));
+ EXPECT_TRUE(InputsAreEqual(decoded2, Input(DECODED_2)));
+}
+
+TEST_F(CTSerializationTest, FailsDecodingInvalidSCTList) {
+ // A list with one item that's too short (the second one)
+ const uint8_t ENCODED[] = {0x00, 0x0a, 0x00, 0x03, 0x61, 0x62,
+ 0x63, 0x00, 0x05, 0x64, 0x65, 0x66};
+
+ Reader listReader;
+ ASSERT_EQ(Success, DecodeSCTList(Input(ENCODED), listReader));
+ Input decoded1;
+ EXPECT_EQ(Success, ReadSCTListItem(listReader, decoded1));
+ Input decoded2;
+ EXPECT_NE(Success, ReadSCTListItem(listReader, decoded2));
+}
+
+TEST_F(CTSerializationTest, EncodesSCTList) {
+ const uint8_t SCT_1[] = {0x61, 0x62, 0x63};
+ const uint8_t SCT_2[] = {0x64, 0x65, 0x66};
+
+ std::vector<Input> list;
+ list.push_back(Input(SCT_1));
+ list.push_back(Input(SCT_2));
+
+ Buffer encodedList;
+ ASSERT_EQ(Success, EncodeSCTList(list, encodedList));
+
+ Reader listReader;
+ ASSERT_EQ(Success, DecodeSCTList(InputForBuffer(encodedList), listReader));
+
+ Input decoded1;
+ ASSERT_EQ(Success, ReadSCTListItem(listReader, decoded1));
+ EXPECT_TRUE(InputsAreEqual(decoded1, Input(SCT_1)));
+
+ Input decoded2;
+ ASSERT_EQ(Success, ReadSCTListItem(listReader, decoded2));
+ EXPECT_TRUE(InputsAreEqual(decoded2, Input(SCT_2)));
+
+ EXPECT_TRUE(listReader.AtEnd());
+}
+
+TEST_F(CTSerializationTest, DecodesSignedCertificateTimestamp) {
+ Buffer encodedSctBuffer = GetTestSignedCertificateTimestamp();
+ Input encodedSctInput = InputForBuffer(encodedSctBuffer);
+ Reader encodedSctReader(encodedSctInput);
+
+ SignedCertificateTimestamp sct;
+ ASSERT_EQ(Success, DecodeSignedCertificateTimestamp(encodedSctReader, sct));
+ EXPECT_EQ(SignedCertificateTimestamp::Version::V1, sct.version);
+ EXPECT_EQ(GetTestPublicKeyId(), sct.logId);
+ const uint64_t expectedTime = 1365181456089;
+ EXPECT_EQ(expectedTime, sct.timestamp);
+ const size_t expectedSignatureLength = 71;
+ EXPECT_EQ(expectedSignatureLength, sct.signature.signatureData.size());
+ EXPECT_TRUE(sct.extensions.empty());
+}
+
+TEST_F(CTSerializationTest, FailsDecodingInvalidSignedCertificateTimestamp) {
+ SignedCertificateTimestamp sct;
+
+ // Invalid version
+ const uint8_t INVALID_VERSION_BYTES[] = {0x02, 0x00};
+ Input invalidVersionSctInput(INVALID_VERSION_BYTES);
+ Reader invalidVersionSctReader(invalidVersionSctInput);
+ EXPECT_EQ(pkix::Result::ERROR_BAD_DER,
+ DecodeSignedCertificateTimestamp(invalidVersionSctReader, sct));
+
+ // Valid version, invalid length (missing data)
+ const uint8_t INVALID_LENGTH_BYTES[] = {0x00, 0x0a, 0x0b, 0x0c};
+ Input invalidLengthSctInput(INVALID_LENGTH_BYTES);
+ Reader invalidLengthSctReader(invalidLengthSctInput);
+ EXPECT_EQ(pkix::Result::ERROR_BAD_DER,
+ DecodeSignedCertificateTimestamp(invalidLengthSctReader, sct));
+}
+
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/tests/gtest/CTTestUtils.cpp b/security/ct/tests/gtest/CTTestUtils.cpp
new file mode 100644
index 0000000000..786c7c190d
--- /dev/null
+++ b/security/ct/tests/gtest/CTTestUtils.cpp
@@ -0,0 +1,820 @@
+/* -*- 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 "CTTestUtils.h"
+
+#include <stdint.h>
+#include <iomanip>
+
+#include "BTTypes.h"
+#include "CTSerialization.h"
+#include "gtest/gtest.h"
+#include "mozpkix/Input.h"
+#include "mozpkix/pkix.h"
+#include "mozpkix/pkixnss.h"
+#include "mozpkix/pkixtypes.h"
+#include "mozpkix/Result.h"
+#include "mozpkix/pkixcheck.h"
+#include "mozpkix/pkixutil.h"
+#include "SignedCertificateTimestamp.h"
+
+namespace mozilla {
+namespace ct {
+
+using namespace mozilla::pkix;
+
+// The following test vectors are from the CT test data repository at
+// https://github.com/google/certificate-transparency/tree/master/test/testdata
+
+// test-cert.pem
+const char kDefaultDerCert[] =
+ "308202ca30820233a003020102020106300d06092a864886f70d01010505003055310b3009"
+ "06035504061302474231243022060355040a131b4365727469666963617465205472616e73"
+ "706172656e6379204341310e300c0603550408130557616c65733110300e06035504071307"
+ "4572772057656e301e170d3132303630313030303030305a170d3232303630313030303030"
+ "305a3052310b30090603550406130247423121301f060355040a1318436572746966696361"
+ "7465205472616e73706172656e6379310e300c0603550408130557616c65733110300e0603"
+ "55040713074572772057656e30819f300d06092a864886f70d010101050003818d00308189"
+ "02818100b1fa37936111f8792da2081c3fe41925008531dc7f2c657bd9e1de4704160b4c9f"
+ "19d54ada4470404c1c51341b8f1f7538dddd28d9aca48369fc5646ddcc7617f8168aae5b41"
+ "d43331fca2dadfc804d57208949061f9eef902ca47ce88c644e000f06eeeccabdc9dd2f68a"
+ "22ccb09dc76e0dbc73527765b1a37a8c676253dcc10203010001a381ac3081a9301d060355"
+ "1d0e041604146a0d982a3b62c44b6d2ef4e9bb7a01aa9cb798e2307d0603551d2304763074"
+ "80145f9d880dc873e654d4f80dd8e6b0c124b447c355a159a4573055310b30090603550406"
+ "1302474231243022060355040a131b4365727469666963617465205472616e73706172656e"
+ "6379204341310e300c0603550408130557616c65733110300e060355040713074572772057"
+ "656e82010030090603551d1304023000300d06092a864886f70d010105050003818100171c"
+ "d84aac414a9a030f22aac8f688b081b2709b848b4e5511406cd707fed028597a9faefc2eee"
+ "2978d633aaac14ed3235197da87e0f71b8875f1ac9e78b281749ddedd007e3ecf50645f8cb"
+ "f667256cd6a1647b5e13203bb8582de7d6696f656d1c60b95f456b7fcf338571908f1c6972"
+ "7d24c4fccd249295795814d1dac0e6";
+
+// key hash of test-cert.pem's issuer (ca-cert.pem)
+const char kDefaultIssuerKeyHash[] =
+ "02adddca08b8bf9861f035940c940156d8350fdff899a6239c6bd77255b8f8fc";
+
+const char kDefaultDerTbsCert[] =
+ "30820233a003020102020107300d06092a864886f70d01010505003055310b300906035504"
+ "061302474231243022060355040a131b4365727469666963617465205472616e7370617265"
+ "6e6379204341310e300c0603550408130557616c65733110300e0603550407130745727720"
+ "57656e301e170d3132303630313030303030305a170d3232303630313030303030305a3052"
+ "310b30090603550406130247423121301f060355040a131843657274696669636174652054"
+ "72616e73706172656e6379310e300c0603550408130557616c65733110300e060355040713"
+ "074572772057656e30819f300d06092a864886f70d010101050003818d0030818902818100"
+ "beef98e7c26877ae385f75325a0c1d329bedf18faaf4d796bf047eb7e1ce15c95ba2f80ee4"
+ "58bd7db86f8a4b252191a79bd700c38e9c0389b45cd4dc9a120ab21e0cb41cd0e72805a410"
+ "cd9c5bdb5d4927726daf1710f60187377ea25b1a1e39eed0b88119dc154dc68f7da8e30caf"
+ "158a33e6c9509f4a05b01409ff5dd87eb50203010001a381ac3081a9301d0603551d0e0416"
+ "04142031541af25c05ffd8658b6843794f5e9036f7b4307d0603551d230476307480145f9d"
+ "880dc873e654d4f80dd8e6b0c124b447c355a159a4573055310b3009060355040613024742"
+ "31243022060355040a131b4365727469666963617465205472616e73706172656e63792043"
+ "41310e300c0603550408130557616c65733110300e060355040713074572772057656e8201"
+ "0030090603551d1304023000";
+
+// DigitallySigned of test-cert.proof
+const char kTestDigitallySigned[] =
+ "0403004730450220606e10ae5c2d5a1b0aed49dc4937f48de71a4e9784e9c208dfbfe9ef53"
+ "6cf7f2022100beb29c72d7d06d61d06bdb38a069469aa86fe12e18bb7cc45689a2c0187ef5"
+ "a5";
+
+// test-cert.proof
+const char kTestSignedCertificateTimestamp[] =
+ "00df1c2ec11500945247a96168325ddc5c7959e8f7c6d388fc002e0bbd3f74d7640000013d"
+ "db27ded900000403004730450220606e10ae5c2d5a1b0aed49dc4937f48de71a4e9784e9c2"
+ "08dfbfe9ef536cf7f2022100beb29c72d7d06d61d06bdb38a069469aa86fe12e18bb7cc456"
+ "89a2c0187ef5a5";
+
+// ct-server-key-public.pem
+const char kEcP256PublicKey[] =
+ "3059301306072a8648ce3d020106082a8648ce3d0301070342000499783cb14533c0161a5a"
+ "b45bf95d08a29cd0ea8dd4c84274e2be59ad15c676960cf0afa1074a57ac644b23479e5b3f"
+ "b7b245eb4b420ef370210371a944beaceb";
+
+// key id (sha256) of ct-server-key-public.pem
+const char kTestKeyId[] =
+ "df1c2ec11500945247a96168325ddc5c7959e8f7c6d388fc002e0bbd3f74d764";
+
+// signature field of DigitallySigned from test-cert.proof
+const char kTestSCTSignatureData[] =
+ "30450220606e10ae5c2d5a1b0aed49dc4937f48de71a4e9784e9c208dfbfe9ef536cf7f202"
+ "2100beb29c72d7d06d61d06bdb38a069469aa86fe12e18bb7cc45689a2c0187ef5a5";
+
+// signature field of DigitallySigned from test-embedded-pre-cert.proof
+const char kTestSCTPrecertSignatureData[] =
+ "30450220482f6751af35dba65436be1fd6640f3dbf9a41429495924530288fa3e5e23e0602"
+ "2100e4edc0db3ac572b1e2f5e8ab6a680653987dcf41027dfeffa105519d89edbf08";
+
+// test-embedded-cert.pem
+const char kTestEmbeddedCertData[] =
+ "30820359308202c2a003020102020107300d06092a864886f70d01010505"
+ "003055310b300906035504061302474231243022060355040a131b436572"
+ "7469666963617465205472616e73706172656e6379204341310e300c0603"
+ "550408130557616c65733110300e060355040713074572772057656e301e"
+ "170d3132303630313030303030305a170d3232303630313030303030305a"
+ "3052310b30090603550406130247423121301f060355040a131843657274"
+ "69666963617465205472616e73706172656e6379310e300c060355040813"
+ "0557616c65733110300e060355040713074572772057656e30819f300d06"
+ "092a864886f70d010101050003818d0030818902818100beef98e7c26877"
+ "ae385f75325a0c1d329bedf18faaf4d796bf047eb7e1ce15c95ba2f80ee4"
+ "58bd7db86f8a4b252191a79bd700c38e9c0389b45cd4dc9a120ab21e0cb4"
+ "1cd0e72805a410cd9c5bdb5d4927726daf1710f60187377ea25b1a1e39ee"
+ "d0b88119dc154dc68f7da8e30caf158a33e6c9509f4a05b01409ff5dd87e"
+ "b50203010001a382013a30820136301d0603551d0e041604142031541af2"
+ "5c05ffd8658b6843794f5e9036f7b4307d0603551d230476307480145f9d"
+ "880dc873e654d4f80dd8e6b0c124b447c355a159a4573055310b30090603"
+ "5504061302474231243022060355040a131b436572746966696361746520"
+ "5472616e73706172656e6379204341310e300c0603550408130557616c65"
+ "733110300e060355040713074572772057656e82010030090603551d1304"
+ "02300030818a060a2b06010401d679020402047c047a0078007600df1c2e"
+ "c11500945247a96168325ddc5c7959e8f7c6d388fc002e0bbd3f74d76400"
+ "00013ddb27df9300000403004730450220482f6751af35dba65436be1fd6"
+ "640f3dbf9a41429495924530288fa3e5e23e06022100e4edc0db3ac572b1"
+ "e2f5e8ab6a680653987dcf41027dfeffa105519d89edbf08300d06092a86"
+ "4886f70d0101050500038181008a0c4bef099d479279afa0a28e689f91e1"
+ "c4421be2d269a2ea6ca4e8215ddeddca1504a11e7c87c4b77e80f0e97903"
+ "5268f27ca20e166804ae556f316981f96a394ab7abfd3e255ac0044513fe"
+ "76570c6795abe4703133d303f89f3afa6bbcfc517319dfd95b934241211f"
+ "634035c3d078307a68c6075a2e20c89f36b8910ca0";
+
+const char kTestTbsCertData[] =
+ "30820233a003020102020107300d06092a864886f70d0101050500305531"
+ "0b300906035504061302474231243022060355040a131b43657274696669"
+ "63617465205472616e73706172656e6379204341310e300c060355040813"
+ "0557616c65733110300e060355040713074572772057656e301e170d3132"
+ "303630313030303030305a170d3232303630313030303030305a3052310b"
+ "30090603550406130247423121301f060355040a13184365727469666963"
+ "617465205472616e73706172656e6379310e300c0603550408130557616c"
+ "65733110300e060355040713074572772057656e30819f300d06092a8648"
+ "86f70d010101050003818d0030818902818100beef98e7c26877ae385f75"
+ "325a0c1d329bedf18faaf4d796bf047eb7e1ce15c95ba2f80ee458bd7db8"
+ "6f8a4b252191a79bd700c38e9c0389b45cd4dc9a120ab21e0cb41cd0e728"
+ "05a410cd9c5bdb5d4927726daf1710f60187377ea25b1a1e39eed0b88119"
+ "dc154dc68f7da8e30caf158a33e6c9509f4a05b01409ff5dd87eb5020301"
+ "0001a381ac3081a9301d0603551d0e041604142031541af25c05ffd8658b"
+ "6843794f5e9036f7b4307d0603551d230476307480145f9d880dc873e654"
+ "d4f80dd8e6b0c124b447c355a159a4573055310b30090603550406130247"
+ "4231243022060355040a131b4365727469666963617465205472616e7370"
+ "6172656e6379204341310e300c0603550408130557616c65733110300e06"
+ "0355040713074572772057656e82010030090603551d1304023000";
+
+// test-embedded-with-preca-cert.pem
+const char kTestEmbeddedWithPreCaCertData[] =
+ "30820359308202c2a003020102020108300d06092a864886f70d01010505"
+ "003055310b300906035504061302474231243022060355040a131b436572"
+ "7469666963617465205472616e73706172656e6379204341310e300c0603"
+ "550408130557616c65733110300e060355040713074572772057656e301e"
+ "170d3132303630313030303030305a170d3232303630313030303030305a"
+ "3052310b30090603550406130247423121301f060355040a131843657274"
+ "69666963617465205472616e73706172656e6379310e300c060355040813"
+ "0557616c65733110300e060355040713074572772057656e30819f300d06"
+ "092a864886f70d010101050003818d0030818902818100afaeeacac51ab7"
+ "cebdf9eacae7dd175295e193955a17989aef8d97ab7cdff7761093c0b823"
+ "d2a4e3a51a17b86f28162b66a2538935ebecdc1036233da2dd6531b0c63b"
+ "cc68761ebdc854037b77399246b870a7b72b14c9b1667de09a9640ed9f3f"
+ "3c725d950b4d26559869fe7f1e919a66eb76d35c0117c6bcd0d8cfd21028"
+ "b10203010001a382013a30820136301d0603551d0e04160414612c64efac"
+ "79b728397c9d93e6df86465fa76a88307d0603551d230476307480145f9d"
+ "880dc873e654d4f80dd8e6b0c124b447c355a159a4573055310b30090603"
+ "5504061302474231243022060355040a131b436572746966696361746520"
+ "5472616e73706172656e6379204341310e300c0603550408130557616c65"
+ "733110300e060355040713074572772057656e82010030090603551d1304"
+ "02300030818a060a2b06010401d679020402047c047a0078007600df1c2e"
+ "c11500945247a96168325ddc5c7959e8f7c6d388fc002e0bbd3f74d76400"
+ "00013ddb27e05b000004030047304502207aa79604c47480f3727b084f90"
+ "b3989f79091885e00484431a2a297cbf3a355c022100b49fd8120b0d644c"
+ "d7e75269b4da6317a9356cb950224fc11cc296b2e39b2386300d06092a86"
+ "4886f70d010105050003818100a3a86c41ad0088a25aedc4e7b529a2ddbf"
+ "9e187ffb362157e9302d961b73b43cba0ae1e230d9e45049b7e8c924792e"
+ "bbe7d175baa87b170dfad8ee788984599d05257994084e2e0e796fca5836"
+ "881c3e053553e06ab230f919089b914e4a8e2da45f8a87f2c81a25a61f04"
+ "fe1cace60155653827d41fad9f0658f287d058192c";
+
+// ca-cert.pem
+const char kCaCertData[] =
+ "308202d030820239a003020102020100300d06092a864886f70d01010505"
+ "003055310b300906035504061302474231243022060355040a131b436572"
+ "7469666963617465205472616e73706172656e6379204341310e300c0603"
+ "550408130557616c65733110300e060355040713074572772057656e301e"
+ "170d3132303630313030303030305a170d3232303630313030303030305a"
+ "3055310b300906035504061302474231243022060355040a131b43657274"
+ "69666963617465205472616e73706172656e6379204341310e300c060355"
+ "0408130557616c65733110300e060355040713074572772057656e30819f"
+ "300d06092a864886f70d010101050003818d0030818902818100d58a6853"
+ "6210a27119936e778321181c2a4013c6d07b8c76eb9157d3d0fb4b3b516e"
+ "cecbd1c98d91c52f743fab635d55099cd13abaf31ae541442451a74c7816"
+ "f2243cf848cf2831cce67ba04a5a23819f3cba37e624d9c3bdb299b839dd"
+ "fe2631d2cb3a84fc7bb2b5c52fcfc14fff406f5cd44669cbb2f7cfdf86fb"
+ "6ab9d1b10203010001a381af3081ac301d0603551d0e041604145f9d880d"
+ "c873e654d4f80dd8e6b0c124b447c355307d0603551d230476307480145f"
+ "9d880dc873e654d4f80dd8e6b0c124b447c355a159a4573055310b300906"
+ "035504061302474231243022060355040a131b4365727469666963617465"
+ "205472616e73706172656e6379204341310e300c0603550408130557616c"
+ "65733110300e060355040713074572772057656e820100300c0603551d13"
+ "040530030101ff300d06092a864886f70d0101050500038181000608cc4a"
+ "6d64f2205e146c04b276f92b0efa94a5daf23afc3806606d3990d0a1ea23"
+ "3d40295769463b046661e7fa1d179915209aea2e0a775176411227d7c003"
+ "07c7470e61584fd7334224727f51d690bc47a9df354db0f6eb25955de189"
+ "3c4dd5202b24a2f3e440d274b54e1bd376269ca96289b76ecaa41090e14f"
+ "3b0a942e";
+
+// intermediate-cert.pem
+const char kIntermediateCertData[] =
+ "308202dd30820246a003020102020109300d06092a864886f70d01010505"
+ "003055310b300906035504061302474231243022060355040a131b436572"
+ "7469666963617465205472616e73706172656e6379204341310e300c0603"
+ "550408130557616c65733110300e060355040713074572772057656e301e"
+ "170d3132303630313030303030305a170d3232303630313030303030305a"
+ "3062310b30090603550406130247423131302f060355040a132843657274"
+ "69666963617465205472616e73706172656e637920496e7465726d656469"
+ "617465204341310e300c0603550408130557616c65733110300e06035504"
+ "0713074572772057656e30819f300d06092a864886f70d01010105000381"
+ "8d0030818902818100d76a678d116f522e55ff821c90642508b7074b14d7"
+ "71159064f7927efdedb87135a1365ee7de18cbd5ce865f860c78f433b4d0"
+ "d3d3407702e7a3ef542b1dfe9bbaa7cdf94dc5975fc729f86f105f381b24"
+ "3535cf9c800f5ca780c1d3c84400ee65d16ee9cf52db8adffe50f5c49335"
+ "0b2190bf50d5bc36f3cac5a8daae92cd8b0203010001a381af3081ac301d"
+ "0603551d0e04160414965508050278479e8773764131bc143a47e229ab30"
+ "7d0603551d230476307480145f9d880dc873e654d4f80dd8e6b0c124b447"
+ "c355a159a4573055310b300906035504061302474231243022060355040a"
+ "131b4365727469666963617465205472616e73706172656e637920434131"
+ "0e300c0603550408130557616c65733110300e0603550407130745727720"
+ "57656e820100300c0603551d13040530030101ff300d06092a864886f70d"
+ "0101050500038181002206dab1c66b71dce095c3f6aa2ef72cf7761be7ab"
+ "d7fc39c31a4cfe1bd96d6734ca82f22dde5a0c8bbbdd825d7b6f3e7612ad"
+ "8db300a7e21169886023262284c3aa5d2191efda10bf9235d37b3a2a340d"
+ "59419b94a48566f3fac3cd8b53d5a4e98270ead297b07210f9ce4a2138b1"
+ "8811143b93fa4e7a87dd37e1385f2c2908";
+
+// test-embedded-with-intermediate-cert.pem
+const char kTestEmbeddedWithIntermediateCertData[] =
+ "30820366308202cfa003020102020102300d06092a864886f70d01010505"
+ "003062310b30090603550406130247423131302f060355040a1328436572"
+ "7469666963617465205472616e73706172656e637920496e7465726d6564"
+ "69617465204341310e300c0603550408130557616c65733110300e060355"
+ "040713074572772057656e301e170d3132303630313030303030305a170d"
+ "3232303630313030303030305a3052310b30090603550406130247423121"
+ "301f060355040a13184365727469666963617465205472616e7370617265"
+ "6e6379310e300c0603550408130557616c65733110300e06035504071307"
+ "4572772057656e30819f300d06092a864886f70d010101050003818d0030"
+ "818902818100bb272b26e5deb5459d4acca027e8f12a4d839ac3730a6a10"
+ "9ff7e25498ddbd3f1895d08ba41f8de34967a3a086ce13a90dd5adbb5418"
+ "4bdc08e1ac7826adb8dc9c717bfd7da5b41b4db1736e00f1dac3cec9819c"
+ "cb1a28ba120b020a820e940dd61f95b5432a4bc05d0818f18ce2154eb38d"
+ "2fa7d22d72b976e560db0c7fc77f0203010001a382013a30820136301d06"
+ "03551d0e04160414b1b148e658e703f5f7f3105f20b3c384d7eff1bf307d"
+ "0603551d23047630748014965508050278479e8773764131bc143a47e229"
+ "aba159a4573055310b300906035504061302474231243022060355040a13"
+ "1b4365727469666963617465205472616e73706172656e6379204341310e"
+ "300c0603550408130557616c65733110300e060355040713074572772057"
+ "656e82010930090603551d130402300030818a060a2b06010401d6790204"
+ "02047c047a0078007600df1c2ec11500945247a96168325ddc5c7959e8f7"
+ "c6d388fc002e0bbd3f74d7640000013ddb27e2a400000403004730450221"
+ "00a6d34517f3392d9ec5d257adf1c597dc45bd4cd3b73856c616a9fb99e5"
+ "ae75a802205e26c8d1c7e222fe8cda29baeb04a834ee97d34fd81718f1aa"
+ "e0cd66f4b8a93f300d06092a864886f70d0101050500038181000f95a5b4"
+ "e128a914b1e88be8b32964221b58f4558433d020a8e246cca65a40bcbf5f"
+ "2d48933ebc99be6927ca756472fb0bdc7f505f41f462f2bc19d0b299c990"
+ "918df8820f3d31db37979e8bad563b17f00ae67b0f8731c106c943a73bf5"
+ "36af168afe21ef4adfcae19a3cc074899992bf506bc5ce1decaaf07ffeeb"
+ "c805c039";
+
+// test-embedded-with-intermediate-preca-cert.pem
+const char kTestEmbeddedWithIntermediatePreCaCertData[] =
+ "30820366308202cfa003020102020103300d06092a864886f70d01010505"
+ "003062310b30090603550406130247423131302f060355040a1328436572"
+ "7469666963617465205472616e73706172656e637920496e7465726d6564"
+ "69617465204341310e300c0603550408130557616c65733110300e060355"
+ "040713074572772057656e301e170d3132303630313030303030305a170d"
+ "3232303630313030303030305a3052310b30090603550406130247423121"
+ "301f060355040a13184365727469666963617465205472616e7370617265"
+ "6e6379310e300c0603550408130557616c65733110300e06035504071307"
+ "4572772057656e30819f300d06092a864886f70d010101050003818d0030"
+ "818902818100d4497056cdfc65e1342cc3df6e654b8af0104702acd2275c"
+ "7d3fb1fc438a89b212110d6419bcc13ae47d64bba241e6706b9ed627f8b3"
+ "4a0d7dff1c44b96287c54bea9d10dc017bceb64f7b6aff3c35a474afec40"
+ "38ab3640b0cd1fb0582ec03b179a2776c8c435d14ab4882d59d7b724fa37"
+ "7ca6db08392173f9c6056b3abadf0203010001a382013a30820136301d06"
+ "03551d0e0416041432da5518d87f1d26ea2767973c0bef286e786a4a307d"
+ "0603551d23047630748014965508050278479e8773764131bc143a47e229"
+ "aba159a4573055310b300906035504061302474231243022060355040a13"
+ "1b4365727469666963617465205472616e73706172656e6379204341310e"
+ "300c0603550408130557616c65733110300e060355040713074572772057"
+ "656e82010930090603551d130402300030818a060a2b06010401d6790204"
+ "02047c047a0078007600df1c2ec11500945247a96168325ddc5c7959e8f7"
+ "c6d388fc002e0bbd3f74d7640000013ddb27e3be00000403004730450221"
+ "00d9f61a07fee021e3159f3ca2f570d833ff01374b2096cba5658c5e16fb"
+ "43eb3002200b76fe475138d8cf76833831304dabf043eb1213c96e13ff4f"
+ "a37f7cd3c8dc1f300d06092a864886f70d01010505000381810088ee4e9e"
+ "5eed6b112cc764b151ed929400e9406789c15fbbcfcdab2f10b400234139"
+ "e6ce65c1e51b47bf7c8950f80bccd57168567954ed35b0ce9346065a5eae"
+ "5bf95d41da8e27cee9eeac688f4bd343f9c2888327abd8b9f68dcb1e3050"
+ "041d31bda8e2dd6d39b3664de5ce0870f5fc7e6a00d6ed00528458d953d2"
+ "37586d73";
+
+// Given the ordered set of data [ 0x00, 0x01, 0x02, deadbeef ],
+// the 'inclusion proof' of the leaf of index '2' (for '0x02') is created from
+// the Merkle Tree generated for that set of data.
+// A Merkle inclusion proof for a leaf in a Merkle Tree is the shortest list
+// of additional nodes in the Merkle Tree required to compute the Merkle Tree
+// Hash (also called 'Merkle Tree head') for that tree.
+// This follows the structure defined in RFC 6962-bis.
+//
+// https://tools.ietf.org/html/draft-ietf-trans-rfc6962-bis-24#section-2.1
+
+const char kTestInclusionProof[] =
+ "020100" // logId
+ "0000000000000004" // tree size
+ "0000000000000002" // leaf index
+ "0042" // inclusion path length
+ "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172"
+ "9" // node
+ // hash
+ // 0
+ "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953"
+ "a"; // node
+ // hash
+ // 1
+
+const char kTestNodeHash0[] =
+ "48c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed321729";
+
+const char kTestNodeHash1[] =
+ "a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953a";
+
+const char kTestInclusionProofUnexpectedData[] = "12345678";
+
+const char kTestInclusionProofInvalidHashSize[] =
+ "020100" // logId
+ "0000000000000004" // treesize
+ "0000000000000002" // leafindex
+ "0042" // inclusion path length
+ "3048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172"
+ "9" // invalid hash size
+ "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953"
+ "a"; // node hash 1
+
+const char kTestInclusionProofInvalidHash[] =
+ "020100" // logId
+ "0000000000000004" // treesize
+ "0000000000000002" // leafindex
+ "0042" // inclusion path length
+ "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172"
+ "9" // node
+ // hash
+ // 0
+ "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427"; // truncated node hash 1
+
+const char kTestInclusionProofMissingLogId[] =
+ "0000000000000004" // treesize
+ "0000000000000002" // leafindex
+ "0042"
+ "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172"
+ "9" // node
+ // hash
+ // 0
+ "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953"
+ "a"; // node
+ // hash
+ // 1
+
+const char kTestInclusionProofNullPathLength[] =
+ "020100"
+ "0000000000000004" // treesize
+ "0000000000000002" // leafindex
+ "0000"
+ "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172"
+ "9" // node
+ // hash
+ // 0
+ "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953"
+ "a"; // node
+ // hash
+ // 1
+
+const char kTestInclusionProofPathLengthTooSmall[] =
+ "020100"
+ "0000000000000004" // treesize
+ "0000000000000002" // leafindex
+ "0036"
+ "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172"
+ "9" // node
+ // hash
+ // 0
+ "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953"
+ "a"; // node
+ // hash
+ // 1
+
+const char kTestInclusionProofPathLengthTooLarge[] =
+ "020100"
+ "0000000000000004" // treesize
+ "0000000000000002" // leafindex
+ "0080"
+ "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172"
+ "9" // node
+ // hash
+ // 0
+ "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953"
+ "a"; // node
+ // hash
+ // 1
+
+const char kTestInclusionProofNullTreeSize[] =
+ "020100"
+ "0000000000000000" // treesize
+ "0000000000000002" // leafindex
+ "0042"
+ "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172"
+ "9" // node
+ // hash
+ // 0
+ "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953"
+ "a"; // node
+ // hash
+ // 1
+
+const char kTestInclusionProofLeafIndexOutOfBounds[] =
+ "020100"
+ "0000000000000004" // treesize
+ "0000000000000004" // leafindex
+ "0042"
+ "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172"
+ "9" // node
+ // hash
+ // 0
+ "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953"
+ "a"; // node
+ // hash
+ // 1
+
+const char kTestInclusionProofExtraData[] =
+ "020100" // logId
+ "0000000000000004" // tree size
+ "0000000000000002" // leaf index
+ "0042" // inclusion path length
+ "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed32172"
+ "9" // node
+ // hash
+ // 0
+ "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953"
+ "a" // node
+ // hash
+ // 1
+ "123456"; // extra data after the proof
+
+static uint8_t CharToByte(char c) {
+ if (c >= '0' && c <= '9') {
+ return c - '0';
+ } else if (c >= 'a' && c <= 'f') {
+ return c - 'a' + 10;
+ } else if (c >= 'A' && c <= 'F') {
+ return c - 'A' + 10;
+ }
+ abort();
+}
+
+Buffer HexToBytes(const char* hexData) {
+ size_t hexLen = strlen(hexData);
+ if (!(hexLen > 0 && (hexLen % 2 == 0))) {
+ abort();
+ }
+ size_t resultLen = hexLen / 2;
+ Buffer result;
+ result.reserve(resultLen);
+ for (size_t i = 0; i < resultLen; ++i) {
+ uint8_t hi = CharToByte(hexData[i * 2]);
+ uint8_t lo = CharToByte(hexData[i * 2 + 1]);
+ result.push_back((hi << 4) | lo);
+ }
+ return result;
+}
+
+void GetX509CertLogEntry(LogEntry& entry) {
+ entry.Reset();
+ entry.type = ct::LogEntry::Type::X509;
+ entry.leafCertificate = HexToBytes(kDefaultDerCert);
+}
+
+Buffer GetDEREncodedX509Cert() { return HexToBytes(kDefaultDerCert); }
+
+void GetPrecertLogEntry(LogEntry& entry) {
+ entry.Reset();
+ entry.type = ct::LogEntry::Type::Precert;
+ entry.issuerKeyHash = HexToBytes(kDefaultIssuerKeyHash);
+ entry.tbsCertificate = HexToBytes(kDefaultDerTbsCert);
+}
+
+Buffer GetTestDigitallySigned() { return HexToBytes(kTestDigitallySigned); }
+
+Buffer GetTestDigitallySignedData() {
+ Buffer encoded = GetTestDigitallySigned();
+ // The encoded buffer contains the signature data itself from the 4th byte.
+ // The first bytes are:
+ // 1 byte of hash algorithm
+ // 1 byte of signature algorithm
+ // 2 bytes - prefix containing length of the signature data.
+ Buffer result;
+ result.assign(encoded.begin() + 4, encoded.end());
+ return result;
+}
+
+Buffer GetTestSignedCertificateTimestamp() {
+ return HexToBytes(kTestSignedCertificateTimestamp);
+}
+
+Buffer GetTestInclusionProof() { return HexToBytes(kTestInclusionProof); }
+
+Buffer GetTestInclusionProofUnexpectedData() {
+ return HexToBytes(kTestInclusionProofUnexpectedData);
+}
+
+Buffer GetTestInclusionProofInvalidHashSize() {
+ return HexToBytes(kTestInclusionProofInvalidHashSize);
+}
+
+Buffer GetTestInclusionProofInvalidHash() {
+ return HexToBytes(kTestInclusionProofInvalidHash);
+}
+
+Buffer GetTestInclusionProofMissingLogId() {
+ return HexToBytes(kTestInclusionProofMissingLogId);
+}
+
+Buffer GetTestInclusionProofNullPathLength() {
+ return HexToBytes(kTestInclusionProofNullPathLength);
+}
+
+Buffer GetTestInclusionProofPathLengthTooSmall() {
+ return HexToBytes(kTestInclusionProofPathLengthTooSmall);
+}
+
+Buffer GetTestInclusionProofPathLengthTooLarge() {
+ return HexToBytes(kTestInclusionProofPathLengthTooLarge);
+}
+
+Buffer GetTestInclusionProofNullTreeSize() {
+ return HexToBytes(kTestInclusionProofNullTreeSize);
+}
+
+Buffer GetTestInclusionProofLeafIndexOutOfBounds() {
+ return HexToBytes(kTestInclusionProofLeafIndexOutOfBounds);
+}
+
+Buffer GetTestInclusionProofExtraData() {
+ return HexToBytes(kTestInclusionProofExtraData);
+}
+
+Buffer GetTestNodeHash0() { return HexToBytes(kTestNodeHash0); }
+
+Buffer GetTestNodeHash1() { return HexToBytes(kTestNodeHash1); }
+
+Buffer GetTestPublicKey() { return HexToBytes(kEcP256PublicKey); }
+
+Buffer GetTestPublicKeyId() { return HexToBytes(kTestKeyId); }
+
+void GetX509CertSCT(SignedCertificateTimestamp& sct) {
+ sct.version = ct::SignedCertificateTimestamp::Version::V1;
+ sct.logId = HexToBytes(kTestKeyId);
+ // Time the log issued a SCT for this certificate, which is
+ // Fri Apr 5 10:04:16.089 2013
+ sct.timestamp = INT64_C(1365181456089);
+ sct.extensions.clear();
+
+ sct.signature.hashAlgorithm = ct::DigitallySigned::HashAlgorithm::SHA256;
+ sct.signature.signatureAlgorithm =
+ ct::DigitallySigned::SignatureAlgorithm::ECDSA;
+ sct.signature.signatureData = HexToBytes(kTestSCTSignatureData);
+}
+
+void GetPrecertSCT(SignedCertificateTimestamp& sct) {
+ sct.version = ct::SignedCertificateTimestamp::Version::V1;
+ sct.logId = HexToBytes(kTestKeyId);
+ // Time the log issued a SCT for this Precertificate, which is
+ // Fri Apr 5 10:04:16.275 2013
+ sct.timestamp = INT64_C(1365181456275);
+ sct.extensions.clear();
+
+ sct.signature.hashAlgorithm = ct::DigitallySigned::HashAlgorithm::SHA256;
+ sct.signature.signatureAlgorithm =
+ ct::DigitallySigned::SignatureAlgorithm::ECDSA;
+ sct.signature.signatureData = HexToBytes(kTestSCTPrecertSignatureData);
+}
+
+Buffer GetDefaultIssuerKeyHash() { return HexToBytes(kDefaultIssuerKeyHash); }
+
+Buffer GetDEREncodedTestEmbeddedCert() {
+ return HexToBytes(kTestEmbeddedCertData);
+}
+
+Buffer GetDEREncodedTestTbsCert() { return HexToBytes(kTestTbsCertData); }
+
+Buffer GetDEREncodedTestEmbeddedWithPreCACert() {
+ return HexToBytes(kTestEmbeddedWithPreCaCertData);
+}
+
+Buffer GetDEREncodedCACert() { return HexToBytes(kCaCertData); }
+
+Buffer GetDEREncodedIntermediateCert() {
+ return HexToBytes(kIntermediateCertData);
+}
+
+Buffer GetDEREncodedTestEmbeddedWithIntermediateCert() {
+ return HexToBytes(kTestEmbeddedWithIntermediateCertData);
+}
+
+Buffer GetDEREncodedTestEmbeddedWithIntermediatePreCACert() {
+ return HexToBytes(kTestEmbeddedWithIntermediatePreCaCertData);
+}
+
+Buffer ExtractCertSPKI(Input cert) {
+ BackCert backCert(cert, EndEntityOrCA::MustBeEndEntity, nullptr);
+ if (backCert.Init() != Success) {
+ abort();
+ }
+
+ Input spkiInput = backCert.GetSubjectPublicKeyInfo();
+ Buffer spki;
+ InputToBuffer(spkiInput, spki);
+ return spki;
+}
+
+Buffer ExtractCertSPKI(const Buffer& cert) {
+ return ExtractCertSPKI(InputForBuffer(cert));
+}
+
+void ExtractEmbeddedSCTList(Input cert, Buffer& result) {
+ result.clear();
+ BackCert backCert(cert, EndEntityOrCA::MustBeEndEntity, nullptr);
+ ASSERT_EQ(Success, backCert.Init());
+ const Input* scts = backCert.GetSignedCertificateTimestamps();
+ if (scts) {
+ Input sctList;
+ ASSERT_EQ(Success, ExtractSignedCertificateTimestampListFromExtension(
+ *scts, sctList));
+ InputToBuffer(sctList, result);
+ }
+}
+
+void ExtractEmbeddedSCTList(const Buffer& cert, Buffer& result) {
+ ExtractEmbeddedSCTList(InputForBuffer(cert), result);
+}
+
+class OCSPExtensionTrustDomain : public TrustDomain {
+ public:
+ pkix::Result GetCertTrust(EndEntityOrCA, const CertPolicyId&, Input,
+ TrustLevel&) override {
+ ADD_FAILURE();
+ return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ pkix::Result FindIssuer(Input, IssuerChecker&, Time) override {
+ ADD_FAILURE();
+ return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ pkix::Result CheckRevocation(EndEntityOrCA, const CertID&, Time, Duration,
+ const Input*, const Input*,
+ const Input*) override {
+ ADD_FAILURE();
+ return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ pkix::Result IsChainValid(const DERArray&, Time,
+ const CertPolicyId&) override {
+ ADD_FAILURE();
+ return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ pkix::Result DigestBuf(Input item, DigestAlgorithm digestAlg,
+ /*out*/ uint8_t* digestBuf,
+ size_t digestBufLen) override {
+ return DigestBufNSS(item, digestAlg, digestBuf, digestBufLen);
+ }
+
+ pkix::Result CheckSignatureDigestAlgorithm(DigestAlgorithm, EndEntityOrCA,
+ Time) override {
+ ADD_FAILURE();
+ return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ pkix::Result CheckECDSACurveIsAcceptable(EndEntityOrCA, NamedCurve) override {
+ ADD_FAILURE();
+ return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ pkix::Result VerifyECDSASignedData(Input data,
+ DigestAlgorithm digestAlgorithm,
+ Input signature,
+ Input subjectPublicKeyInfo) override {
+ return VerifyECDSASignedDataNSS(data, digestAlgorithm, signature,
+ subjectPublicKeyInfo, nullptr);
+ }
+
+ pkix::Result CheckRSAPublicKeyModulusSizeInBits(EndEntityOrCA,
+ unsigned int) override {
+ ADD_FAILURE();
+ return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ pkix::Result VerifyRSAPKCS1SignedData(Input data,
+ DigestAlgorithm digestAlgorithm,
+ Input signature,
+ Input subjectPublicKeyInfo) override {
+ return VerifyRSAPKCS1SignedDataNSS(data, digestAlgorithm, signature,
+ subjectPublicKeyInfo, nullptr);
+ }
+
+ pkix::Result VerifyRSAPSSSignedData(Input data,
+ DigestAlgorithm digestAlgorithm,
+ Input signature,
+ Input subjectPublicKeyInfo) override {
+ return VerifyRSAPSSSignedDataNSS(data, digestAlgorithm, signature,
+ subjectPublicKeyInfo, nullptr);
+ }
+
+ pkix::Result CheckValidityIsAcceptable(Time, Time, EndEntityOrCA,
+ KeyPurposeId) override {
+ ADD_FAILURE();
+ return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ pkix::Result NetscapeStepUpMatchesServerAuth(Time, bool&) override {
+ ADD_FAILURE();
+ return pkix::Result::FATAL_ERROR_LIBRARY_FAILURE;
+ }
+
+ void NoteAuxiliaryExtension(AuxiliaryExtension extension,
+ Input data) override {
+ if (extension != AuxiliaryExtension::SCTListFromOCSPResponse) {
+ ADD_FAILURE();
+ return;
+ }
+ InputToBuffer(data, signedCertificateTimestamps);
+ }
+
+ Buffer signedCertificateTimestamps;
+};
+
+void ExtractSCTListFromOCSPResponse(Input cert, Input issuerSPKI,
+ Input encodedResponse, Time time,
+ Buffer& result) {
+ result.clear();
+
+ BackCert backCert(cert, EndEntityOrCA::MustBeEndEntity, nullptr);
+ ASSERT_EQ(Success, backCert.Init());
+
+ CertID certID(backCert.GetIssuer(), issuerSPKI, backCert.GetSerialNumber());
+
+ bool expired;
+ OCSPExtensionTrustDomain trustDomain;
+ pkix::Result rv =
+ VerifyEncodedOCSPResponse(trustDomain, certID, time, /*time*/
+ 1000, /*maxLifetimeInDays*/
+ encodedResponse, expired);
+ ASSERT_EQ(Success, rv);
+
+ result = std::move(trustDomain.signedCertificateTimestamps);
+}
+
+Input InputForBuffer(const Buffer& buffer) {
+ Input input;
+ if (input.Init(buffer.data(), buffer.size()) != Success) {
+ abort();
+ }
+ return input;
+}
+
+Input InputForSECItem(const SECItem& item) {
+ Input input;
+ if (input.Init(item.data, item.len) != Success) {
+ abort();
+ }
+ return input;
+}
+
+} // namespace ct
+} // namespace mozilla
+
+namespace mozilla {
+
+std::ostream& operator<<(std::ostream& stream, const ct::Buffer& buffer) {
+ if (buffer.empty()) {
+ stream << "EMPTY";
+ } else {
+ for (size_t i = 0; i < buffer.size(); ++i) {
+ if (i >= 1000) {
+ stream << "...";
+ break;
+ }
+ stream << std::hex << std::setw(2) << std::setfill('0')
+ << static_cast<unsigned>(buffer[i]);
+ }
+ }
+ stream << std::dec;
+ return stream;
+}
+
+} // namespace mozilla
diff --git a/security/ct/tests/gtest/CTTestUtils.h b/security/ct/tests/gtest/CTTestUtils.h
new file mode 100644
index 0000000000..3679d4411e
--- /dev/null
+++ b/security/ct/tests/gtest/CTTestUtils.h
@@ -0,0 +1,137 @@
+/* -*- 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 CTTestUtils_h
+#define CTTestUtils_h
+
+#include <iostream>
+
+#include "mozpkix/Input.h"
+#include "mozpkix/Time.h"
+#include "seccomon.h"
+#include "SignedCertificateTimestamp.h"
+
+namespace mozilla {
+namespace ct {
+
+Buffer HexToBytes(const char* hexData);
+
+// Note: unless specified otherwise, all test data is taken from
+// Certificate Transparency test data repository at
+// https://github.com/google/certificate-transparency/tree/master/test/testdata
+
+// Fills |entry| with test data for an X.509 entry.
+void GetX509CertLogEntry(LogEntry& entry);
+
+// Returns a DER-encoded X509 cert. The SCT provided by
+// GetX509CertSCT is signed over this certificate.
+Buffer GetDEREncodedX509Cert();
+
+// Fills |entry| with test data for a Precertificate entry.
+void GetPrecertLogEntry(LogEntry& entry);
+
+// Returns the binary representation of a test DigitallySigned.
+Buffer GetTestDigitallySigned();
+
+// Returns the source data of the test DigitallySigned.
+Buffer GetTestDigitallySignedData();
+
+// Returns the binary representation of a test serialized SCT.
+Buffer GetTestSignedCertificateTimestamp();
+
+// Returns the binary representation of a test serialized InclusionProof.
+Buffer GetTestInclusionProof();
+Buffer GetTestInclusionProofUnexpectedData();
+Buffer GetTestInclusionProofInvalidHashSize();
+Buffer GetTestInclusionProofInvalidHash();
+Buffer GetTestInclusionProofMissingLogId();
+Buffer GetTestInclusionProofNullPathLength();
+Buffer GetTestInclusionProofPathLengthTooSmall();
+Buffer GetTestInclusionProofPathLengthTooLarge();
+Buffer GetTestInclusionProofNullTreeSize();
+Buffer GetTestInclusionProofLeafIndexOutOfBounds();
+Buffer GetTestInclusionProofExtraData();
+
+// Returns the binary representation of test serialized node hashs from an
+// inclusion proof.
+Buffer GetTestNodeHash0();
+Buffer GetTestNodeHash1();
+
+// Test log key.
+Buffer GetTestPublicKey();
+
+// ID of test log key.
+Buffer GetTestPublicKeyId();
+
+// SCT for the X509Certificate provided above.
+void GetX509CertSCT(SignedCertificateTimestamp& sct);
+
+// SCT for the Precertificate log entry provided above.
+void GetPrecertSCT(SignedCertificateTimestamp& sct);
+
+// Issuer key hash.
+Buffer GetDefaultIssuerKeyHash();
+
+// The SHA256 root hash for the sample STH.
+Buffer GetSampleSTHSHA256RootHash();
+
+// The tree head signature for the sample STH.
+Buffer GetSampleSTHTreeHeadSignature();
+
+// The same signature as GetSampleSTHTreeHeadSignature, decoded.
+void GetSampleSTHTreeHeadDecodedSignature(DigitallySigned& signature);
+
+// Certificate with embedded SCT in an X509v3 extension.
+Buffer GetDEREncodedTestEmbeddedCert();
+
+// For the above certificate, the corresponsing TBSCertificate without
+// the embedded SCT extension.
+Buffer GetDEREncodedTestTbsCert();
+
+// As above, but signed with an intermediate CA certificate containing
+// the CT extended key usage OID 1.3.6.1.4.1.11129.2.4.4 for issuing precerts
+// (i.e. signed with a "precert CA certificate").
+Buffer GetDEREncodedTestEmbeddedWithPreCACert();
+
+// The issuer of the above certificates (self-signed root CA certificate).
+Buffer GetDEREncodedCACert();
+
+// An intermediate CA certificate issued by the above CA.
+Buffer GetDEREncodedIntermediateCert();
+
+// Certificate with embedded SCT signed by the intermediate certificate above.
+Buffer GetDEREncodedTestEmbeddedWithIntermediateCert();
+
+// As above, but signed by the precert CA certificate.
+Buffer GetDEREncodedTestEmbeddedWithIntermediatePreCACert();
+
+// Given a DER-encoded certificate, returns its SubjectPublicKeyInfo.
+Buffer ExtractCertSPKI(pkix::Input cert);
+Buffer ExtractCertSPKI(const Buffer& cert);
+
+// Extracts a SignedCertificateTimestampList from the provided leaf certificate
+// (kept in X.509v3 extension with OID 1.3.6.1.4.1.11129.2.4.2).
+void ExtractEmbeddedSCTList(pkix::Input cert, Buffer& result);
+void ExtractEmbeddedSCTList(const Buffer& cert, Buffer& result);
+
+// Extracts a SignedCertificateTimestampList that has been embedded within
+// an OCSP response as an extension with the OID 1.3.6.1.4.1.11129.2.4.5.
+// The OCSP response is verified, and the verification must succeed for the
+// extension to be extracted.
+void ExtractSCTListFromOCSPResponse(pkix::Input cert, pkix::Input issuerSPKI,
+ pkix::Input encodedResponse,
+ pkix::Time time, Buffer& result);
+
+// Returns Input for the data stored in the buffer, failing assertion on error.
+pkix::Input InputForBuffer(const Buffer& buffer);
+
+// Returns Input for the data stored in the item, failing assertion on error.
+pkix::Input InputForSECItem(const SECItem& item);
+
+} // namespace ct
+} // namespace mozilla
+
+#endif // CTTestUtils_h
diff --git a/security/ct/tests/gtest/MultiLogCTVerifierTest.cpp b/security/ct/tests/gtest/MultiLogCTVerifierTest.cpp
new file mode 100644
index 0000000000..4ac3f42f28
--- /dev/null
+++ b/security/ct/tests/gtest/MultiLogCTVerifierTest.cpp
@@ -0,0 +1,254 @@
+/* -*- 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 "MultiLogCTVerifier.h"
+
+#include <stdint.h>
+
+#include "CTLogVerifier.h"
+#include "CTObjectsExtractor.h"
+#include "CTSerialization.h"
+#include "CTTestUtils.h"
+#include "gtest/gtest.h"
+#include "nss.h"
+
+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.
+ if (NSS_NoDB_Init(nullptr) != SECSuccess) {
+ abort();
+ }
+
+ CTLogVerifier log;
+ ASSERT_EQ(Success,
+ log.Init(InputForBuffer(GetTestPublicKey()), mLogOperatorID,
+ CTLogStatus::Included, 0 /*disqualification time*/));
+ mVerifier.AddLog(std::move(log));
+
+ mTestCert = GetDEREncodedX509Cert();
+ mEmbeddedCert = GetDEREncodedTestEmbeddedCert();
+ mCaCert = GetDEREncodedCACert();
+ mCaCertSPKI = ExtractCertSPKI(mCaCert);
+ mIntermediateCert = GetDEREncodedIntermediateCert();
+ mIntermediateCertSPKI = ExtractCertSPKI(mIntermediateCert);
+
+ // Set the current time making sure all test timestamps are in the past.
+ mNow =
+ TimeFromEpochInSeconds(1451606400u); // Date.parse("2016-01-01")/1000
+ }
+
+ void CheckForSingleValidSCTInResult(const CTVerifyResult& result,
+ VerifiedSCT::Origin origin) {
+ EXPECT_EQ(0U, result.decodingErrors);
+ ASSERT_EQ(1U, result.verifiedScts.size());
+ 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) {
+ std::vector<Input> list;
+ list.push_back(std::move(sct));
+ ASSERT_EQ(Success, EncodeSCTList(list, output));
+ }
+
+ void GetSCTListWithInvalidLogID(Buffer& result) {
+ result.clear();
+ Buffer sct(GetTestSignedCertificateTimestamp());
+ // Change a byte inside the Log ID part of the SCT so it does
+ // not match the log used in the tests.
+ sct[15] ^= '\xFF';
+ EncodeSCTListForTesting(InputForBuffer(sct), result);
+ }
+
+ void CheckPrecertVerification(const Buffer& cert, const Buffer& issuerSPKI) {
+ Buffer sctList;
+ ExtractEmbeddedSCTList(cert, sctList);
+ ASSERT_FALSE(sctList.empty());
+
+ CTVerifyResult result;
+ ASSERT_EQ(Success,
+ mVerifier.Verify(InputForBuffer(cert), InputForBuffer(issuerSPKI),
+ InputForBuffer(sctList), Input(), Input(), mNow,
+ result));
+ CheckForSingleValidSCTInResult(result, VerifiedSCT::Origin::Embedded);
+ }
+
+ 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;
+
+ // Extract the embedded SCT.
+
+ Buffer sctList;
+ ExtractEmbeddedSCTList(mEmbeddedCert, sctList);
+ ASSERT_FALSE(sctList.empty());
+
+ Reader sctReader;
+ ASSERT_EQ(Success, DecodeSCTList(InputForBuffer(sctList), sctReader));
+ Input sctItemInput;
+ ASSERT_EQ(Success, ReadSCTListItem(sctReader, sctItemInput));
+ EXPECT_TRUE(sctReader.AtEnd()); // we only expect one sct in the list
+
+ Reader sctItemReader(sctItemInput);
+ ASSERT_EQ(Success, DecodeSignedCertificateTimestamp(sctItemReader, sct));
+
+ // Make sure the SCT contains the expected data.
+
+ EXPECT_EQ(SignedCertificateTimestamp::Version::V1, sct.version);
+ EXPECT_EQ(GetTestPublicKeyId(), sct.logId);
+
+ uint64_t expectedTimestamp = 1365181456275;
+ EXPECT_EQ(expectedTimestamp, sct.timestamp);
+}
+
+TEST_F(MultiLogCTVerifierTest, VerifiesEmbeddedSCT) {
+ CheckPrecertVerification(mEmbeddedCert, mCaCertSPKI);
+}
+
+TEST_F(MultiLogCTVerifierTest, VerifiesEmbeddedSCTWithPreCA) {
+ CheckPrecertVerification(GetDEREncodedTestEmbeddedWithPreCACert(),
+ mCaCertSPKI);
+}
+
+TEST_F(MultiLogCTVerifierTest, VerifiesEmbeddedSCTWithIntermediate) {
+ CheckPrecertVerification(GetDEREncodedTestEmbeddedWithIntermediateCert(),
+ mIntermediateCertSPKI);
+}
+
+TEST_F(MultiLogCTVerifierTest, VerifiesEmbeddedSCTWithIntermediateAndPreCA) {
+ CheckPrecertVerification(GetDEREncodedTestEmbeddedWithIntermediatePreCACert(),
+ mIntermediateCertSPKI);
+}
+
+TEST_F(MultiLogCTVerifierTest, VerifiesSCTFromOCSP) {
+ Buffer sct(GetTestSignedCertificateTimestamp());
+ Buffer sctList;
+ EncodeSCTListForTesting(InputForBuffer(sct), sctList);
+
+ CTVerifyResult result;
+ ASSERT_EQ(Success,
+ mVerifier.Verify(InputForBuffer(mTestCert), Input(), Input(),
+ InputForBuffer(sctList), Input(), mNow, result));
+
+ CheckForSingleValidSCTInResult(result, VerifiedSCT::Origin::OCSPResponse);
+}
+
+TEST_F(MultiLogCTVerifierTest, VerifiesSCTFromTLS) {
+ Buffer sct(GetTestSignedCertificateTimestamp());
+ Buffer sctList;
+ EncodeSCTListForTesting(InputForBuffer(sct), sctList);
+
+ CTVerifyResult result;
+ ASSERT_EQ(Success,
+ mVerifier.Verify(InputForBuffer(mTestCert), Input(), Input(),
+ Input(), InputForBuffer(sctList), mNow, result));
+
+ CheckForSingleValidSCTInResult(result, VerifiedSCT::Origin::TLSExtension);
+}
+
+TEST_F(MultiLogCTVerifierTest, VerifiesSCTFromMultipleSources) {
+ Buffer sct(GetTestSignedCertificateTimestamp());
+ Buffer sctList;
+ EncodeSCTListForTesting(InputForBuffer(sct), sctList);
+
+ CTVerifyResult result;
+ ASSERT_EQ(Success, mVerifier.Verify(InputForBuffer(mTestCert), Input(),
+ Input(), InputForBuffer(sctList),
+ InputForBuffer(sctList), mNow, result));
+
+ // The result should contain verified SCTs from TLS and OCSP origins.
+ size_t embeddedCount = 0;
+ size_t tlsExtensionCount = 0;
+ size_t ocspResponseCount = 0;
+ for (const VerifiedSCT& verifiedSct : result.verifiedScts) {
+ EXPECT_EQ(VerifiedSCT::Status::Valid, verifiedSct.status);
+ switch (verifiedSct.origin) {
+ case VerifiedSCT::Origin::Embedded:
+ embeddedCount++;
+ break;
+ case VerifiedSCT::Origin::TLSExtension:
+ tlsExtensionCount++;
+ break;
+ case VerifiedSCT::Origin::OCSPResponse:
+ ocspResponseCount++;
+ break;
+ case VerifiedSCT::Origin::Unknown:
+ default:
+ ASSERT_TRUE(false);
+ }
+ }
+ EXPECT_EQ(embeddedCount, 0u);
+ EXPECT_TRUE(tlsExtensionCount > 0);
+ EXPECT_TRUE(ocspResponseCount > 0);
+}
+
+TEST_F(MultiLogCTVerifierTest, IdentifiesSCTFromUnknownLog) {
+ Buffer sctList;
+ GetSCTListWithInvalidLogID(sctList);
+
+ CTVerifyResult result;
+ ASSERT_EQ(Success,
+ mVerifier.Verify(InputForBuffer(mTestCert), Input(), Input(),
+ Input(), InputForBuffer(sctList), mNow, result));
+
+ EXPECT_EQ(0U, result.decodingErrors);
+ ASSERT_EQ(1U, result.verifiedScts.size());
+ 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));
+ verifier.AddLog(std::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.size());
+ EXPECT_EQ(VerifiedSCT::Status::ValidFromDisqualifiedLog,
+ result.verifiedScts[0].status);
+ EXPECT_EQ(disqualificationTime,
+ result.verifiedScts[0].logDisqualificationTime);
+ EXPECT_EQ(mLogOperatorID, result.verifiedScts[0].logOperatorId);
+}
+
+} // namespace ct
+} // namespace mozilla
diff --git a/security/ct/tests/gtest/createSTHTestData.py b/security/ct/tests/gtest/createSTHTestData.py
new file mode 100755
index 0000000000..ab61d4ba0e
--- /dev/null
+++ b/security/ct/tests/gtest/createSTHTestData.py
@@ -0,0 +1,133 @@
+#!/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/.
+
+"""
+This utility is used by the build system to create test inputs for the
+signed tree head decoding and verification implementation. The format is
+generally lines of <key>:<value> pairs except for the to-be-signed
+section, which consists of one or more lines of hex bytes. Comments may
+appear at the end of lines and begin with '//'.
+The following keys are valid:
+signingKey: A pykey key identifier to use to sign the to-be-signed data.
+ Required.
+spki: A pykey key identifier to create an encoded SubjectPublicKeyInfo
+ to be included with the test data. The tests will use this spki to
+ validate the signature. Required.
+prefix: Hex bytes to include at the beginning of the signed tree head
+ data. This data is not covered by the signature (typically this
+ is used for the log_id field). Optional. Defaults to the empty
+ string.
+hash: The name of a hash algorithm to use when signing. Optional.
+ Defaults to 'sha256'.
+"""
+
+import binascii
+import os
+import sys
+
+from pyasn1.codec.der import encoder
+
+sys.path.append(
+ os.path.join(os.path.dirname(__file__), "..", "..", "..", "manager", "tools")
+)
+import pykey
+
+
+def sign(signingKey, hashAlgorithm, hexToSign):
+ """Given a pykey, the name of a hash function, and hex bytes to
+ sign, signs the data (as binary) and returns a hex string consisting
+ of the signature."""
+ # key.sign returns a hex string in the format "'<hex bytes>'H",
+ # so we have to strip off the "'"s and trailing 'H'
+ return signingKey.sign(binascii.unhexlify(hexToSign), "hash:%s" % hashAlgorithm)[
+ 1:-2
+ ]
+
+
+class Error(Exception):
+ """Base class for exceptions in this module."""
+
+ pass
+
+
+class UnknownParameterTypeError(Error):
+ """Base class for handling unexpected input in this module."""
+
+ def __init__(self, value):
+ super(Error, self).__init__()
+ self.value = value
+ self.category = "key"
+
+ def __str__(self):
+ return 'Unknown %s type "%s"' % (self.category, repr(self.value))
+
+
+class InputTooLongError(Error):
+ """Helper exception type for inputs that are too long."""
+
+ def __init__(self, length):
+ super(InputTooLongError, self).__init__()
+ self.length = length
+
+ def __str__(self):
+ return "Input too long: %s > 65535" % self.length
+
+
+def getTwoByteLenAsHex(callLenOnMe):
+ """Given something that len can be called on, returns a hex string
+ representing the two-byte length of the something, in network byte
+ order (the length must be less than or equal to 65535)."""
+ length = len(callLenOnMe)
+ if length > 65535:
+ raise InputTooLongError(length)
+ return bytes([length // 256, length % 256]).hex()
+
+
+def createSTH(configStream):
+ """Given a stream that will provide the specification for a signed
+ tree head (see the comment at the top of this file), creates the
+ corresponding signed tree head. Returns a string that can be
+ compiled as C/C++ that declares two const char*s kSTHHex and
+ kSPKIHex corresponding to the hex encoding of the signed tree head
+ and the hex encoding of the subject public key info from the
+ specification, respectively."""
+ toSign = ""
+ prefix = ""
+ hashAlgorithm = "sha256"
+ for line in configStream.readlines():
+ if ":" in line:
+ param = line.split(":")[0]
+ arg = line.split(":")[1].split("//")[0].strip()
+ if param == "signingKey":
+ signingKey = pykey.keyFromSpecification(arg)
+ elif param == "spki":
+ spki = pykey.keyFromSpecification(arg)
+ elif param == "prefix":
+ prefix = arg
+ elif param == "hash":
+ hashAlgorithm = arg
+ else:
+ raise UnknownParameterTypeError(param)
+ else:
+ toSign = toSign + line.split("//")[0].strip()
+ signature = sign(signingKey, hashAlgorithm, toSign)
+ lengthBytesHex = getTwoByteLenAsHex(binascii.unhexlify(signature))
+ sth = prefix + toSign + lengthBytesHex + signature
+ spkiHex = encoder.encode(spki.asSubjectPublicKeyInfo()).hex()
+ return 'const char* kSTHHex = "%s";\nconst char* kSPKIHex = "%s";\n' % (
+ sth,
+ spkiHex,
+ )
+
+
+def main(output, inputPath):
+ """Given a file-like output and the path to a signed tree head
+ specification (see the comment at the top of this file), reads the
+ specification, creates the signed tree head, and outputs test data
+ that can be included by a gtest corresponding to the
+ specification."""
+ with open(inputPath) as configStream:
+ output.write(createSTH(configStream))
diff --git a/security/ct/tests/gtest/ec-signer-rsa-spki-sth.inc.tbs b/security/ct/tests/gtest/ec-signer-rsa-spki-sth.inc.tbs
new file mode 100644
index 0000000000..a8593e27c4
--- /dev/null
+++ b/security/ct/tests/gtest/ec-signer-rsa-spki-sth.inc.tbs
@@ -0,0 +1,8 @@
+signingKey:secp256r1
+spki:default // this is the default 2048-bit RSA key
+prefix:020000
+00000166d6142b50 // 1541189938000 milliseconds since the epoch
+0000000000000007 // 7 total nodes
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc
+0000 // no extensions
diff --git a/security/ct/tests/gtest/missing-extensions-sth.inc.tbs b/security/ct/tests/gtest/missing-extensions-sth.inc.tbs
new file mode 100644
index 0000000000..98807b854d
--- /dev/null
+++ b/security/ct/tests/gtest/missing-extensions-sth.inc.tbs
@@ -0,0 +1,8 @@
+signingKey:secp256r1
+spki:secp256r1
+prefix:020000
+00000166d6142b50 // 1541189938000 milliseconds since the epoch
+0000000000000007 // 7 total nodes
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc
+// missing extensions
diff --git a/security/ct/tests/gtest/missing-log-id-sth.inc.tbs b/security/ct/tests/gtest/missing-log-id-sth.inc.tbs
new file mode 100644
index 0000000000..6c1bb21997
--- /dev/null
+++ b/security/ct/tests/gtest/missing-log-id-sth.inc.tbs
@@ -0,0 +1,8 @@
+signingKey:secp256r1
+spki:secp256r1
+// missing logid
+00000166d6142b50 // 1541189938000 milliseconds since the epoch
+0000000000000007 // 7 total nodes
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc
+0000 // no extensions
diff --git a/security/ct/tests/gtest/missing-root-hash-sth.inc.tbs b/security/ct/tests/gtest/missing-root-hash-sth.inc.tbs
new file mode 100644
index 0000000000..5accf70fe8
--- /dev/null
+++ b/security/ct/tests/gtest/missing-root-hash-sth.inc.tbs
@@ -0,0 +1,7 @@
+signingKey:secp256r1
+spki:secp256r1
+prefix:020000
+00000166d6142b50 // 1541189938000 milliseconds since the epoch
+0000000000000007 // 7 total nodes
+// missing root_hash
+0000 // no extensions
diff --git a/security/ct/tests/gtest/missing-timestamp-sth.inc.tbs b/security/ct/tests/gtest/missing-timestamp-sth.inc.tbs
new file mode 100644
index 0000000000..5e7ebb6cf2
--- /dev/null
+++ b/security/ct/tests/gtest/missing-timestamp-sth.inc.tbs
@@ -0,0 +1,8 @@
+signingKey:secp256r1
+spki:secp256r1
+prefix:020000
+// missing timestamp
+0000000000000007 // 7 total nodes
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc
+0000 // no extensions
diff --git a/security/ct/tests/gtest/missing-tree-size-sth.inc.tbs b/security/ct/tests/gtest/missing-tree-size-sth.inc.tbs
new file mode 100644
index 0000000000..128e6d64b1
--- /dev/null
+++ b/security/ct/tests/gtest/missing-tree-size-sth.inc.tbs
@@ -0,0 +1,8 @@
+signingKey:secp256r1
+spki:secp256r1
+prefix:020000
+00000166d6142b50 // 1541189938000 milliseconds since the epoch
+// missing tree_size
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc
+0000 // no extensions
diff --git a/security/ct/tests/gtest/moz.build b/security/ct/tests/gtest/moz.build
new file mode 100644
index 0000000000..4c0ed38557
--- /dev/null
+++ b/security/ct/tests/gtest/moz.build
@@ -0,0 +1,56 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# 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/.
+
+
+@template
+def STHTestFile(name):
+ if not CONFIG["COMPILE_ENVIRONMENT"]:
+ return
+
+ GeneratedFile(name, script="createSTHTestData.py", inputs=["%s.tbs" % name])
+
+
+STHTestFile("valid-sth.inc")
+STHTestFile("valid-with-extension-sth.inc")
+STHTestFile("valid-secp521r1-sha512-sth.inc")
+STHTestFile("signature-covers-log-id-sth.inc")
+STHTestFile("wrong-spki-sth.inc")
+STHTestFile("wrong-signing-key-sth.inc")
+STHTestFile("missing-log-id-sth.inc")
+STHTestFile("missing-timestamp-sth.inc")
+STHTestFile("missing-tree-size-sth.inc")
+STHTestFile("missing-root-hash-sth.inc")
+STHTestFile("missing-extensions-sth.inc")
+STHTestFile("truncated-log-id-sth.inc")
+STHTestFile("truncated-timestamp-sth.inc")
+STHTestFile("truncated-tree-size-sth.inc")
+STHTestFile("truncated-root-hash-sth.inc")
+STHTestFile("truncated-extension-sth.inc")
+STHTestFile("rsa-signer-rsa-spki-sth.inc")
+STHTestFile("rsa-signer-ec-spki-sth.inc")
+STHTestFile("ec-signer-rsa-spki-sth.inc")
+
+UNIFIED_SOURCES += [
+ "BTSerializationTest.cpp",
+ "BTSignedTreeHeadTest.cpp",
+ "BTVerificationTest.cpp",
+ "CTDiversityPolicyTest.cpp",
+ "CTLogVerifierTest.cpp",
+ "CTObjectsExtractorTest.cpp",
+ "CTPolicyEnforcerTest.cpp",
+ "CTSerializationTest.cpp",
+ "CTTestUtils.cpp",
+ "MultiLogCTVerifierTest.cpp",
+]
+
+LOCAL_INCLUDES += [
+ "../..",
+]
+
+if not CONFIG["MOZ_DEBUG"]:
+ DEFINES["NDEBUG"] = True
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/security/ct/tests/gtest/rsa-signer-ec-spki-sth.inc.tbs b/security/ct/tests/gtest/rsa-signer-ec-spki-sth.inc.tbs
new file mode 100644
index 0000000000..92f1a47490
--- /dev/null
+++ b/security/ct/tests/gtest/rsa-signer-ec-spki-sth.inc.tbs
@@ -0,0 +1,8 @@
+signingKey:default // this is the default 2048-bit RSA key
+spki:secp256r1
+prefix:020000
+00000166d6142b50 // 1541189938000 milliseconds since the epoch
+0000000000000007 // 7 total nodes
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc
+0000 // no extensions
diff --git a/security/ct/tests/gtest/rsa-signer-rsa-spki-sth.inc.tbs b/security/ct/tests/gtest/rsa-signer-rsa-spki-sth.inc.tbs
new file mode 100644
index 0000000000..b412c87e12
--- /dev/null
+++ b/security/ct/tests/gtest/rsa-signer-rsa-spki-sth.inc.tbs
@@ -0,0 +1,8 @@
+signingKey:default // this is the default 2048-bit RSA key
+spki:default
+prefix:020000
+00000166d6142b50 // 1541189938000 milliseconds since the epoch
+0000000000000007 // 7 total nodes
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc
+0000 // no extensions
diff --git a/security/ct/tests/gtest/signature-covers-log-id-sth.inc.tbs b/security/ct/tests/gtest/signature-covers-log-id-sth.inc.tbs
new file mode 100644
index 0000000000..723ee7a978
--- /dev/null
+++ b/security/ct/tests/gtest/signature-covers-log-id-sth.inc.tbs
@@ -0,0 +1,8 @@
+signingKey:secp256r1
+spki:secp256r1
+020000 // This is the log id. It should not be covered by the signature, but it is in this case.
+00000166d6142b50 // 1541189938000 milliseconds since the epoch
+0000000000000007 // 7 total nodes
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc
+0000 // no extensions
diff --git a/security/ct/tests/gtest/truncated-extension-sth.inc.tbs b/security/ct/tests/gtest/truncated-extension-sth.inc.tbs
new file mode 100644
index 0000000000..0fe30a58da
--- /dev/null
+++ b/security/ct/tests/gtest/truncated-extension-sth.inc.tbs
@@ -0,0 +1,11 @@
+signingKey:secp256r1
+spki:secp256r1
+prefix:020000
+00000166d6142b50 // 1541189938000 milliseconds since the epoch
+0000000000000007 // 7 total nodes
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc
+0014 // 20 bytes of extensions
+ ffff // extension type ffff
+ 0010 // 16 bytes of data
+ ffffffffff // truncated
diff --git a/security/ct/tests/gtest/truncated-log-id-sth.inc.tbs b/security/ct/tests/gtest/truncated-log-id-sth.inc.tbs
new file mode 100644
index 0000000000..267976af44
--- /dev/null
+++ b/security/ct/tests/gtest/truncated-log-id-sth.inc.tbs
@@ -0,0 +1,8 @@
+signingKey:secp256r1
+spki:secp256r1
+prefix:0200 // truncated log_id
+00000166d6142b50 // 1541189938000 milliseconds since the epoch
+0000000000000007 // 7 total nodes
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc
+0000 // no extensions
diff --git a/security/ct/tests/gtest/truncated-root-hash-sth.inc.tbs b/security/ct/tests/gtest/truncated-root-hash-sth.inc.tbs
new file mode 100644
index 0000000000..295b069880
--- /dev/null
+++ b/security/ct/tests/gtest/truncated-root-hash-sth.inc.tbs
@@ -0,0 +1,8 @@
+signingKey:secp256r1
+spki:secp256r1
+prefix:020000
+00000166d6142b50 // 1541189938000 milliseconds since the epoch
+0000000000000007 // 7 total nodes
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e0 // truncated
+0000 // no extensions
diff --git a/security/ct/tests/gtest/truncated-timestamp-sth.inc.tbs b/security/ct/tests/gtest/truncated-timestamp-sth.inc.tbs
new file mode 100644
index 0000000000..247bd4dddc
--- /dev/null
+++ b/security/ct/tests/gtest/truncated-timestamp-sth.inc.tbs
@@ -0,0 +1,8 @@
+signingKey:secp256r1
+spki:secp256r1
+prefix:020000
+00000166d6 // truncated timestamp
+0000000000000007 // 7 total nodes
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc
+0000 // no extensions
diff --git a/security/ct/tests/gtest/truncated-tree-size-sth.inc.tbs b/security/ct/tests/gtest/truncated-tree-size-sth.inc.tbs
new file mode 100644
index 0000000000..9e0a942827
--- /dev/null
+++ b/security/ct/tests/gtest/truncated-tree-size-sth.inc.tbs
@@ -0,0 +1,8 @@
+signingKey:secp256r1
+spki:secp256r1
+prefix:020000
+00000166d6142b50 // 1541189938000 milliseconds since the epoch
+000000 // truncate tree_size
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc
+0000 // no extensions
diff --git a/security/ct/tests/gtest/valid-secp521r1-sha512-sth.inc.tbs b/security/ct/tests/gtest/valid-secp521r1-sha512-sth.inc.tbs
new file mode 100644
index 0000000000..674adfec56
--- /dev/null
+++ b/security/ct/tests/gtest/valid-secp521r1-sha512-sth.inc.tbs
@@ -0,0 +1,10 @@
+signingKey:secp521r1
+spki:secp521r1
+hash:sha512
+prefix:020000
+000001670e7ca6e1 // 1542136309473 milliseconds since the epoch
+000000002b982da5 // 731393445 total nodes
+40 // 64 byte hash
+ 374d794a95cdcfd8b35993185fef9ba368f160d8daf432d08ba9f1ed1e5abe6c
+ c69291e0fa2fe0006a52570ef18c19def4e617c33ce52ef0a6e5fbe318cb0387
+0000 // no extensions
diff --git a/security/ct/tests/gtest/valid-sth.inc.tbs b/security/ct/tests/gtest/valid-sth.inc.tbs
new file mode 100644
index 0000000000..b1bb592e93
--- /dev/null
+++ b/security/ct/tests/gtest/valid-sth.inc.tbs
@@ -0,0 +1,8 @@
+signingKey:secp256r1
+spki:secp256r1
+prefix:020000
+00000166d6142b50 // 1541189938000 milliseconds since the epoch
+0000000000000007 // 7 total nodes
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc
+0000 // no extensions
diff --git a/security/ct/tests/gtest/valid-with-extension-sth.inc.tbs b/security/ct/tests/gtest/valid-with-extension-sth.inc.tbs
new file mode 100644
index 0000000000..62612de301
--- /dev/null
+++ b/security/ct/tests/gtest/valid-with-extension-sth.inc.tbs
@@ -0,0 +1,11 @@
+signingKey:secp256r1
+spki:secp256r1
+prefix:020000
+00000166d6142b50 // 1541189938000 milliseconds since the epoch
+0000000000000007 // 7 total nodes
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc
+0014 // 20 bytes of extensions
+ ffff // extension type ffff
+ 0010 // 16 bytes of data
+ ffffffffffffffffffffffffffffffff
diff --git a/security/ct/tests/gtest/wrong-signing-key-sth.inc.tbs b/security/ct/tests/gtest/wrong-signing-key-sth.inc.tbs
new file mode 100644
index 0000000000..b8a93bb39e
--- /dev/null
+++ b/security/ct/tests/gtest/wrong-signing-key-sth.inc.tbs
@@ -0,0 +1,8 @@
+signingKey:secp384r1
+spki:secp256r1
+prefix:020000
+00000166d6142b50 // 1541189938000 milliseconds since the epoch
+0000000000000007 // 7 total nodes
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc
+0000 // no extensions
diff --git a/security/ct/tests/gtest/wrong-spki-sth.inc.tbs b/security/ct/tests/gtest/wrong-spki-sth.inc.tbs
new file mode 100644
index 0000000000..c02282c904
--- /dev/null
+++ b/security/ct/tests/gtest/wrong-spki-sth.inc.tbs
@@ -0,0 +1,8 @@
+signingKey:secp256r1
+spki:secp384r1
+prefix:020000
+00000166d6142b50 // 1541189938000 milliseconds since the epoch
+0000000000000007 // 7 total nodes
+20 // 32 byte hash
+ d1a0d3947db4ae8305f2ac32985957e02659b2ea3c10da52a48d2526e9af3bbc
+0000 // no extensions