Bug 1343202 - Utility function for decoding an InclusionProof structure; r=keeler, r?ckerschb draft
authorStephanie Ouillon <stephouillon@mozilla.com>
Fri, 18 Aug 2017 09:50:49 +0200
changeset 648731 38585e2674c196483abbfc95607a1ced99291036
parent 648270 18d8f7469a3b67b3205995a29a32ead613bca77c
child 726919 6308645399d3df0f5670b9c5959218a439fd2282
push id74859
push userbmo:stephouillon@mozilla.com
push dateFri, 18 Aug 2017 07:54:56 +0000
reviewerskeeler, ckerschb
bugs1343202
milestone57.0a1
Bug 1343202 - Utility function for decoding an InclusionProof structure; r=keeler, r?ckerschb MozReview-Commit-ID: 1x2Cwan8nLL
security/certverifier/BTInclusionProof.h
security/certverifier/BTVerifier.cpp
security/certverifier/BTVerifier.h
security/certverifier/CTSerialization.cpp
security/certverifier/CTUtils.h
security/certverifier/moz.build
security/certverifier/tests/gtest/BTSerializationTest.cpp
security/certverifier/tests/gtest/CTSerializationTest.cpp
security/certverifier/tests/gtest/CTTestUtils.cpp
security/certverifier/tests/gtest/CTTestUtils.h
security/certverifier/tests/gtest/moz.build
new file mode 100644
--- /dev/null
+++ b/security/certverifier/BTInclusionProof.h
@@ -0,0 +1,41 @@
+/* -*- 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 BTInclusionProof_h
+#define BTInclusionProof_h
+
+#include "Buffer.h"
+#include "mozilla/Vector.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;
+
+const uint64_t kInitialPathLengthCapacity = 32;
+
+struct InclusionProofDataV2
+{
+  Buffer logId;
+  uint64_t treeSize;
+  uint64_t leafIndex;
+  Vector<Buffer, kInitialPathLengthCapacity> inclusionPath;
+};
+
+} } // namespace mozilla:ct
+
+#endif // BTInclusionProof_h
new file mode 100644
--- /dev/null
+++ b/security/certverifier/BTVerifier.cpp
@@ -0,0 +1,104 @@
+/* -*- 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 "CTUtils.h"
+#include "SignedCertificateTimestamp.h"
+
+#include <stdint.h>
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Move.h"
+#include "mozilla/TypeTraits.h"
+
+namespace mozilla { namespace ct {
+
+using namespace mozilla::pkix;
+
+typedef mozilla::pkix::Result Result;
+
+// Members of a Inclusion Proof struct
+static const size_t kLogIdPrefixLengthBytes = 1;
+static const size_t kProofTreeSizeLength = 8;
+static const size_t kLeafIndexLength = 8;
+static const size_t kInclusionPathLengthBytes = 2;
+static const size_t kNodeHashPrefixLengthBytes = 1;
+
+Result
+DecodeInclusionProof(pkix::Reader& reader, InclusionProofDataV2& output)
+{
+  InclusionProofDataV2 result;
+
+  Input logId;
+  Result rv = ReadVariableBytes<kLogIdPrefixLengthBytes>(reader, logId);
+  if (rv != Success) {
+    return rv;
+  }
+
+  rv = ReadUint<kProofTreeSizeLength>(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);
+  Vector<Buffer, kInitialPathLengthCapacity> inclusionPath;
+
+  while (!pathReader.AtEnd()) {
+    Input hash;
+    rv = ReadVariableBytes<kNodeHashPrefixLengthBytes>(pathReader, hash);
+    if (rv != Success) {
+      return rv;
+    }
+
+    Buffer hashBuffer;
+    rv = InputToBuffer(hash, hashBuffer);
+    if (rv != Success) {
+      return rv;
+    }
+
+    if (!inclusionPath.append(Move(hashBuffer))) {
+      return pkix::Result::FATAL_ERROR_NO_MEMORY;
+    }
+  }
+
+  if (!reader.AtEnd()){
+    return pkix::Result::ERROR_BAD_DER;
+  }
+
+  rv = InputToBuffer(logId, result.logId);
+  if (rv != Success) {
+    return rv;
+  }
+
+  result.inclusionPath = Move(inclusionPath);
+
+  output = Move(result);
+  return Success;
+}
+} } //namespace mozilla::ct
new file mode 100644
--- /dev/null
+++ b/security/certverifier/BTVerifier.h
@@ -0,0 +1,23 @@
+/* -*- 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 "BTInclusionProof.h"
+#include "pkix/Input.h"
+#include "pkix/Result.h"
+
+namespace mozilla { namespace ct {
+
+// Decodes an Inclusion Proof (InclusionProofDataV2 as defined in RFC
+// 6962-bis). This consumes the entirety of the input.
+pkix::Result DecodeInclusionProof(pkix::Reader& input,
+  InclusionProofDataV2& output);
+
+} } // namespace mozilla::ct
+
+#endif // BTVerifier_h
--- a/security/certverifier/CTSerialization.cpp
+++ b/security/certverifier/CTSerialization.cpp
@@ -1,15 +1,16 @@
 /* -*- 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 "mozilla/Assertions.h"
 #include "mozilla/Move.h"
 #include "mozilla/TypeTraits.h"
 
 namespace mozilla { namespace ct {
@@ -67,17 +68,17 @@ UncheckedReadUint(size_t length, Reader&
     result = (result << 8) | value;
   }
   out = result;
   return Success;
 }
 
 // Performs overflow sanity checks and calls UncheckedReadUint.
 template <size_t length, typename T>
-static inline Result
+Result
 ReadUint(Reader& in, T& out)
 {
   uint64_t value;
   static_assert(mozilla::IsUnsigned<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");
   Result rv = UncheckedReadUint(length, in, value);
   if (rv != Success) {
@@ -93,17 +94,17 @@ ReadFixedBytes(size_t length, Reader& in
 {
   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>
-static inline Result
+Result
 ReadVariableBytes(Reader& in, Input& out)
 {
   size_t length;
   Result rv = ReadUint<prefixLength>(in, length);
   if (rv != Success) {
     return rv;
   }
   return ReadFixedBytes(length, in, out);
new file mode 100644
--- /dev/null
+++ b/security/certverifier/CTUtils.h
@@ -0,0 +1,30 @@
+/* -*- 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 "pkix/Input.h"
+#include "pkix/Result.h"
+
+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: checks if the output parameter overflows while reading.
+// |length| indicates the size (in bytes) of the serialized integer.
+template <size_t length, typename T>
+pkix::Result ReadUint(Reader& in, T& 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(Reader& in, Input& out);
+
+} } // namespace mozilla::ct
+
+#endif //CTUtils_h
--- a/security/certverifier/moz.build
+++ b/security/certverifier/moz.build
@@ -4,28 +4,31 @@
 # 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 += [
     'BRNameMatchingPolicy.h',
+    'BTInclusionProof.h',
+    'BTVerifier.h',
     'Buffer.h',
     'CertVerifier.h',
     'CTLog.h',
     'CTPolicyEnforcer.h',
     'CTVerifyResult.h',
     'OCSPCache.h',
     'SignedCertificateTimestamp.h',
     'SignedTreeHead.h',
 ]
 
 UNIFIED_SOURCES += [
     'BRNameMatchingPolicy.cpp',
+    'BTVerifier.cpp',
     'Buffer.cpp',
     'CertVerifier.cpp',
     'CTDiversityPolicy.cpp',
     'CTLogVerifier.cpp',
     'CTObjectsExtractor.cpp',
     'CTPolicyEnforcer.cpp',
     'CTSerialization.cpp',
     'CTVerifyResult.cpp',
new file mode 100644
--- /dev/null
+++ b/security/certverifier/tests/gtest/BTSerializationTest.cpp
@@ -0,0 +1,160 @@
+/* -*- 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;
+
+  const uint8_t EXPECTED_LOGID[] = { 0x01, 0x00 };
+  Buffer expectedLogId;
+  MOZ_RELEASE_ASSERT(expectedLogId.append(EXPECTED_LOGID, 2));
+
+
+  Input encodedProofInput = InputForBuffer(mTestInclusionProof);
+  Reader encodedProofReader(encodedProofInput);
+
+  InclusionProofDataV2 ipr;
+  ASSERT_EQ(Success, DecodeInclusionProof(encodedProofReader, ipr));
+  EXPECT_EQ(expectedLogId, ipr.logId);
+  EXPECT_EQ(expectedTreeSize, ipr.treeSize);
+  EXPECT_EQ(expectedLeafIndex, ipr.leafIndex);
+  EXPECT_EQ(expectedInclusionPathElements, ipr.inclusionPath.length());
+  EXPECT_EQ(GetTestNodeHash0(), ipr.inclusionPath[0]);
+  EXPECT_EQ(GetTestNodeHash1(), ipr.inclusionPath[1]);
+}
+
+TEST_F(BTSerializationTest, FailsDecodingInclusionProofUnexpectedData)
+{
+  Input encodedProofInput = InputForBuffer(mTestInclusionProofUnexpectedData);
+  Reader encodedProofReader(encodedProofInput);
+  InclusionProofDataV2 ipr;
+
+  ASSERT_EQ(Result::ERROR_BAD_DER, DecodeInclusionProof(encodedProofReader, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingInvalidHashSize)
+{
+  Input encodedProofInput = InputForBuffer(mTestInclusionProofInvalidHashSize);
+  Reader encodedProofReader(encodedProofInput);
+  InclusionProofDataV2 ipr;
+
+  ASSERT_EQ(Result::ERROR_BAD_DER, DecodeInclusionProof(encodedProofReader, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingInvalidHash)
+{
+  Input encodedProofInput = InputForBuffer(mTestInclusionProofInvalidHash);
+  Reader encodedProofReader(encodedProofInput);
+  InclusionProofDataV2 ipr;
+
+  ASSERT_EQ(Result::ERROR_BAD_DER, DecodeInclusionProof(encodedProofReader, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingMissingLogId)
+{
+  Input encodedProofInput = InputForBuffer(mTestInclusionProofMissingLogId);
+  Reader encodedProofReader(encodedProofInput);
+  InclusionProofDataV2 ipr;
+
+  ASSERT_EQ(Result::ERROR_BAD_DER, DecodeInclusionProof(encodedProofReader, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingNullPathLength)
+{
+  Input encodedProofInput = InputForBuffer(mTestInclusionProofNullPathLength);
+  Reader encodedProofReader(encodedProofInput);
+  InclusionProofDataV2 ipr;
+
+  ASSERT_EQ(Result::ERROR_BAD_DER, DecodeInclusionProof(encodedProofReader, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingPathLengthTooSmall)
+{
+  Input encodedProofInput = InputForBuffer(mTestInclusionProofPathLengthTooSmall);
+  Reader encodedProofReader(encodedProofInput);
+  InclusionProofDataV2 ipr;
+
+  ASSERT_EQ(Result::ERROR_BAD_DER, DecodeInclusionProof(encodedProofReader, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingPathLengthTooLarge)
+{
+  Input encodedProofInput = InputForBuffer(mTestInclusionProofPathLengthTooLarge);
+  Reader encodedProofReader(encodedProofInput);
+  InclusionProofDataV2 ipr;
+
+  ASSERT_EQ(Result::ERROR_BAD_DER, DecodeInclusionProof(encodedProofReader, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingNullTreeSize)
+{
+  Input encodedProofInput = InputForBuffer(mTestInclusionProofNullTreeSize);
+  Reader encodedProofReader(encodedProofInput);
+  InclusionProofDataV2 ipr;
+
+  ASSERT_EQ(Result::ERROR_BAD_DER, DecodeInclusionProof(encodedProofReader, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingLeafIndexOutOfBounds)
+{
+  Input encodedProofInput = InputForBuffer(mTestInclusionProofLeafIndexOutOfBounds);
+  Reader encodedProofReader(encodedProofInput);
+  InclusionProofDataV2 ipr;
+
+  ASSERT_EQ(Result::ERROR_BAD_DER, DecodeInclusionProof(encodedProofReader, ipr));
+}
+
+TEST_F(BTSerializationTest, FailsDecodingExtraData)
+{
+  Input encodedProofInput = InputForBuffer(mTestInclusionProofExtraData);
+  Reader encodedProofReader(encodedProofInput);
+  InclusionProofDataV2 ipr;
+
+  ASSERT_EQ(Result::ERROR_BAD_DER, DecodeInclusionProof(encodedProofReader, ipr));
+}
+} } // namespace mozilla::ct
--- a/security/certverifier/tests/gtest/CTSerializationTest.cpp
+++ b/security/certverifier/tests/gtest/CTSerializationTest.cpp
@@ -262,10 +262,9 @@ TEST_F(CTSerializationTest, EncodesValid
     // sha256 root hash should follow
   };
   Buffer expectedBuffer;
   MOZ_RELEASE_ASSERT(expectedBuffer.append(EXPECTED_BYTES_PREFIX, 18));
   Buffer hash = GetSampleSTHSHA256RootHash();
   MOZ_RELEASE_ASSERT(expectedBuffer.append(hash.begin(), hash.length()));
   EXPECT_EQ(expectedBuffer, encoded);
 }
-
-} }  // namespace mozilla::ct
+} } // namespace mozilla::ct
--- a/security/certverifier/tests/gtest/CTTestUtils.cpp
+++ b/security/certverifier/tests/gtest/CTTestUtils.cpp
@@ -4,16 +4,17 @@
  * 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 "BTInclusionProof.h"
 #include "CTSerialization.h"
 #include "gtest/gtest.h"
 #include "mozilla/Assertions.h"
 #include "mozilla/Move.h"
 #include "mozilla/Vector.h"
 #include "pkix/Input.h"
 #include "pkix/pkix.h"
 #include "pkix/pkixnss.h"
@@ -321,16 +322,114 @@ const char kTestEmbeddedWithIntermediate
   "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
+  "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed321729" // node hash 0
+  "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953a"; // 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
+  "3048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed321729" // invalid hash size
+  "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953a"; // node hash 1
+
+const char kTestInclusionProofInvalidHash[] =
+  "020100" // logId
+  "0000000000000004" // treesize
+  "0000000000000002" // leafindex
+  "0042" // inclusion path length
+  "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed321729" // node hash 0
+  "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427"; // truncated node hash 1
+
+const char kTestInclusionProofMissingLogId[] =
+  "0000000000000004" // treesize
+  "0000000000000002" // leafindex
+  "0042"
+  "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed321729" // node hash 0
+  "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953a"; // node hash 1
+
+const char kTestInclusionProofNullPathLength[] =
+  "020100"
+  "0000000000000004" // treesize
+  "0000000000000002" // leafindex
+  "0000"
+  "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed321729" // node hash 0
+  "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953a"; // node hash 1
+
+const char kTestInclusionProofPathLengthTooSmall[] =
+  "020100"
+  "0000000000000004" // treesize
+  "0000000000000002" // leafindex
+  "0036"
+  "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed321729" // node hash 0
+  "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953a"; // node hash 1
+
+const char kTestInclusionProofPathLengthTooLarge[] =
+  "020100"
+  "0000000000000004" // treesize
+  "0000000000000002" // leafindex
+  "0080"
+  "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed321729" // node hash 0
+  "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953a"; // node hash 1
+
+const char kTestInclusionProofNullTreeSize[] =
+  "020100"
+  "0000000000000000" // treesize
+  "0000000000000002" // leafindex
+  "0042"
+  "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed321729" // node hash 0
+  "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953a"; // node hash 1
+
+const char kTestInclusionProofLeafIndexOutOfBounds[] =
+  "020100"
+  "0000000000000004" // treesize
+  "0000000000000004" // leafindex
+  "0042"
+  "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed321729" // node hash 0
+  "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953a"; // node hash 1
+
+const char kTestInclusionProofExtraData[] =
+  "020100" // logId
+  "0000000000000004" // tree size
+  "0000000000000002" // leaf index
+  "0042" // inclusion path length
+  "2048c90c8ae24688d6bef5d48a30c2cc8b6754335a8db21793cc0a8e3bed321729" // node hash 0
+  "20a20bf9a7cc2dc8a08f5f415a71b19f6ac427bab54d24eec868b5d3103449953a" // 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') {
@@ -402,16 +501,94 @@ GetTestDigitallySignedData()
 
 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()
 {
--- a/security/certverifier/tests/gtest/CTTestUtils.h
+++ b/security/certverifier/tests/gtest/CTTestUtils.h
@@ -35,16 +35,34 @@ void GetPrecertLogEntry(LogEntry& entry)
 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);
--- a/security/certverifier/tests/gtest/moz.build
+++ b/security/certverifier/tests/gtest/moz.build
@@ -1,15 +1,16 @@
 # -*- 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/.
 
 SOURCES += [
+    'BTSerializationTest.cpp',
     'CTDiversityPolicyTest.cpp',
     'CTLogVerifierTest.cpp',
     'CTObjectsExtractorTest.cpp',
     'CTPolicyEnforcerTest.cpp',
     'CTSerializationTest.cpp',
     'CTTestUtils.cpp',
     'MultiLogCTVerifierTest.cpp',
 ]