Bug 1241574 - Certificate Transparency - base definitions and serialization to/from TLS wire format. r=keeler, r=Cykesiopka
authorSergei Chernov <sergei.cv@ndivi.com>
Mon, 11 Apr 2016 16:17:25 +0300
changeset 337853 35b845bf58f1abf677ae61393bc5f593c0a0b40f
parent 337852 d33cfe343732ae76307eb77504b28a813bd9f27c
child 337854 80c71070fc08c90b9131ec676d7d021506d381d2
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskeeler, Cykesiopka
bugs1241574
milestone49.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1241574 - Certificate Transparency - base definitions and serialization to/from TLS wire format. r=keeler, r=Cykesiopka MozReview-Commit-ID: KmJOr2crof7
security/certverifier/CTSerialization.cpp
security/certverifier/CTSerialization.h
security/certverifier/SignedCertificateTimestamp.cpp
security/certverifier/SignedCertificateTimestamp.h
security/certverifier/SignedTreeHead.h
security/certverifier/moz.build
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/CTSerialization.cpp
@@ -0,0 +1,504 @@
+/* -*- 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 <stdint.h>
+
+#include "mozilla/Assertions.h"
+#include "mozilla/TypeTraits.h"
+
+namespace mozilla { namespace ct {
+
+using namespace mozilla::pkix;
+
+// 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;
+
+// Members of digitally-signed struct of a STH
+static const size_t kTreeSizeLength = 8;
+
+// Length of sha256RootHash buffer of SignedTreeHead
+static const size_t kSthRootHashLength = 32;
+
+enum class SignatureType {
+  CertificateTimestamp = 0,
+  TreeHash = 1,
+};
+
+// 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.
+static Result
+UncheckedReadUint(size_t length, Reader& in, uint64_t& out)
+{
+  uint64_t result = 0;
+  for (size_t i = 0; i < length; ++i) {
+    uint8_t value;
+    Result rv = in.Read(value);
+    if (rv != Success) {
+      return rv;
+    }
+    result = (result << 8) | value;
+  }
+  out = result;
+  return Success;
+}
+
+// Performs overflow sanity checks and calls UncheckedReadUint.
+template <size_t length, typename T>
+static inline 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) {
+    return rv;
+  }
+  out = static_cast<T>(value);
+  return Success;
+}
+
+// Reads |length| bytes from |in|.
+static Result
+ReadFixedBytes(size_t length, Reader& in, 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>
+static inline 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);
+}
+
+// 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)
+{
+  if (!output.reserve(length + output.length())) {
+    return Result::FATAL_ERROR_NO_MEMORY;
+  }
+  for (; length > 0; --length) {
+    uint8_t nextByte = (value >> ((length - 1) * 8)) & 0xFF;
+    output.infallibleAppend(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 (mozilla::IsSigned<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 Result
+WriteEncodedBytes(Input input, Buffer& output)
+{
+  if (!output.append(input.UnsafeGetData(), input.GetLength())) {
+    return Result::FATAL_ERROR_NO_MEMORY;
+  }
+  return Success;
+}
+
+// Same as above, but the source data is in a Buffer.
+static Result
+WriteEncodedBytes(const Buffer& source, Buffer& output)
+{
+  if (!output.appendAll(source)) {
+    return Result::FATAL_ERROR_NO_MEMORY;
+  }
+  return Success;
+}
+
+// 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)
+{
+  size_t inputSize = input.GetLength();
+  const size_t maxAllowedInputSize =
+    static_cast<size_t>(((1 << (prefixLength * 8)) - 1));
+  if (inputSize > maxAllowedInputSize) {
+    return Result::FATAL_ERROR_INVALID_ARGS;
+  }
+
+  Result rv = WriteUint<prefixLength>(inputSize, output);
+  if (rv != Success) {
+    return rv;
+  }
+  return WriteEncodedBytes(input, output);
+}
+
+// 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.length() != kLogIdLength) {
+    return Result::FATAL_ERROR_INVALID_ARGS;
+  }
+  Result rv = WriteEncodedBytes(entry.issuerKeyHash, output);
+  if (rv != Success) {
+    return rv;
+  }
+  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;
+  }
+  rv = InputToBuffer(signatureData, result.signatureData);
+  if (rv != Success) {
+    return rv;
+  }
+
+  output = 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:
+      MOZ_ASSERT_UNREACHABLE("Unexpected LogEntry type");
+  }
+  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.
+  rv = WriteEncodedBytes(serializedLogEntry, output);
+  if (rv != Success) {
+    return rv;
+  }
+  return WriteVariableBytes<kExtensionsLengthBytes>(extensions, output);
+}
+
+Result
+EncodeTreeHeadSignature(const SignedTreeHead& signedTreeHead,
+                        Buffer& output)
+{
+  Result rv = WriteUint<kVersionLength>(
+    static_cast<unsigned int>(signedTreeHead.version), output);
+  if (rv != Success) {
+    return rv;
+  }
+  rv = WriteUint<kSignatureTypeLength>(
+    static_cast<unsigned int>(SignatureType::TreeHash), output);
+  if (rv != Success) {
+    return rv;
+  }
+  rv = WriteTimeSinceEpoch(signedTreeHead.timestamp, output);
+  if (rv != Success) {
+    return rv;
+  }
+  rv = WriteUint<kTreeSizeLength>(signedTreeHead.treeSize, output);
+  if (rv != Success) {
+    return rv;
+  }
+  if (signedTreeHead.sha256RootHash.length() != kSthRootHashLength) {
+    return Result::FATAL_ERROR_INVALID_ARGS;
+  }
+  return WriteEncodedBytes(signedTreeHead.sha256RootHash, 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;
+  }
+
+  rv = InputToBuffer(logId, result.logId);
+  if (rv != Success) {
+    return rv;
+  }
+  rv = InputToBuffer(extensions, result.extensions);
+  if (rv != Success) {
+    return rv;
+  }
+  result.timestamp = timestamp;
+
+  output = Move(result);
+  return Success;
+}
+
+} } // namespace mozilla::ct
new file mode 100644
--- /dev/null
+++ b/security/certverifier/CTSerialization.h
@@ -0,0 +1,66 @@
+/* -*- 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 "pkix/Input.h"
+#include "pkix/Result.h"
+#include "SignedCertificateTimestamp.h"
+#include "SignedTreeHead.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);
+
+// Encodes the data signed by a Signed Tree Head (STH) |signedTreeHead| into
+// |output|. The signature included in the |signedTreeHead| can then be
+// verified over these bytes.
+pkix::Result EncodeTreeHeadSignature(const SignedTreeHead& signedTreeHead,
+                                     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);
+
+} } // namespace mozilla::ct
+
+#endif // CTSerialization_h
new file mode 100644
--- /dev/null
+++ b/security/certverifier/SignedCertificateTimestamp.cpp
@@ -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/. */
+
+#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 mozilla::ct
+
+
+namespace mozilla {
+
+bool
+operator==(const ct::Buffer& a, const ct::Buffer& b)
+{
+  return (a.empty() && b.empty()) ||
+    (a.length() == b.length() && memcmp(a.begin(), b.begin(), a.length()) == 0);
+}
+
+bool
+operator!=(const ct::Buffer& a, const ct::Buffer& b) {
+  return !(a == b);
+}
+
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/security/certverifier/SignedCertificateTimestamp.h
@@ -0,0 +1,125 @@
+/* -*- 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 "mozilla/Vector.h"
+
+#include "pkix/Input.h"
+#include "pkix/Result.h"
+
+// Structures related to Certificate Transparency (RFC 6962).
+namespace mozilla { namespace ct {
+
+typedef Vector<uint8_t> Buffer;
+
+// 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,
+  };
+
+  // Source of the SCT - supplementary, not defined in CT RFC.
+  // Note: The numeric values are used within histograms and should not change
+  // or be re-assigned.
+  enum class Origin {
+    Embedded = 0,
+    TLSExtension = 1,
+    OCSPResponse = 2,
+  };
+
+  Version version;
+  Buffer logId;
+  uint64_t timestamp;
+  Buffer extensions;
+  DigitallySigned signature;
+  Origin origin;
+};
+
+
+inline pkix::Result BufferToInput(const Buffer& buffer, pkix::Input& input)
+{
+  return input.Init(buffer.begin(), buffer.length());
+}
+
+inline pkix::Result InputToBuffer(pkix::Input input, Buffer& buffer)
+{
+  buffer.clear();
+  if (!buffer.append(input.UnsafeGetData(), input.GetLength())) {
+    return pkix::Result::FATAL_ERROR_NO_MEMORY;
+  }
+  return pkix::Success;
+}
+
+} } // namespace mozilla::ct
+
+namespace mozilla {
+
+// Comparison operators are placed under mozilla namespace since
+// mozilla::ct::Buffer is actually mozilla::Vector.
+bool operator==(const ct::Buffer& a, const ct::Buffer& b);
+bool operator!=(const ct::Buffer& a, const ct::Buffer& b);
+
+} // namespace mozilla
+
+#endif // SignedCertificateTimestamp_h
new file mode 100644
--- /dev/null
+++ b/security/certverifier/SignedTreeHead.h
@@ -0,0 +1,31 @@
+/* -*- 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 SignedTreeHead_h
+#define SignedTreeHead_h
+
+#include "SignedCertificateTimestamp.h"
+
+namespace mozilla { namespace ct {
+
+// Signed Tree Head as defined in section 3.5. of RFC-6962.
+struct SignedTreeHead
+{
+  // Version enum in RFC 6962, Section 3.2. Note that while in the current
+  // RFC the STH and SCT share the versioning scheme, there are plans in
+  // RFC-6962-bis to use separate versions, so using a separate scheme here.
+  enum class Version { V1 = 0, };
+
+  Version version;
+  uint64_t timestamp;
+  uint64_t treeSize;
+  Buffer sha256RootHash;
+  DigitallySigned signature;
+};
+
+} } // namespace mozilla::ct
+
+#endif // SignedTreeHead_h
--- a/security/certverifier/moz.build
+++ b/security/certverifier/moz.build
@@ -8,36 +8,42 @@ EXPORTS += [
     'BRNameMatchingPolicy.h',
     'CertVerifier.h',
     'OCSPCache.h',
 ]
 
 UNIFIED_SOURCES += [
     'BRNameMatchingPolicy.cpp',
     'CertVerifier.cpp',
+    'CTSerialization.cpp',
     'NSSCertDBTrustDomain.cpp',
     'OCSPCache.cpp',
     'OCSPRequestor.cpp',
     'OCSPVerificationTrustDomain.cpp',
+    'SignedCertificateTimestamp.cpp',
 ]
 
 if not CONFIG['NSS_NO_EV_CERTS']:
     UNIFIED_SOURCES += [
         'ExtendedValidation.cpp',
     ]
 
 LOCAL_INCLUDES += [
     '/security/manager/ssl',
     '/security/pkix/include',
 ]
 
 DIRS += [
     '../pkix',
 ]
 
+TEST_DIRS += [
+    'tests/gtest',
+]
+
 CXXFLAGS += ['-Wall']
 if CONFIG['_MSC_VER']:
   # -Wall with Visual C++ enables too many problematic warnings
   CXXFLAGS += [
     '-wd4355', # 'this' used in base member initializer list
     '-wd4464', # relative include path contains '..'
     '-wd4480', # nonstandard extension used: specifying underlying type for
                # enum 'enum'
new file mode 100644
--- /dev/null
+++ b/security/certverifier/tests/gtest/CTSerializationTest.cpp
@@ -0,0 +1,245 @@
+/* -*- 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.begin(),
+      mTestDigitallySigned.length() - 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 = cloneBuffer(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.length());
+  // First two bytes are log entry type. Next, length:
+  // Length is 718 which is 512 + 206, which is 0x2ce
+  Buffer expectedPrefix;
+  MOZ_RELEASE_ASSERT(expectedPrefix.append("\0\0\0\x2\xCE", 5));
+  Buffer encodedPrefix;
+  MOZ_RELEASE_ASSERT(encodedPrefix.
+    append(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.length()), encoded.length());
+
+  // First two bytes are log entry type.
+  Buffer expectedPrefix;
+  MOZ_RELEASE_ASSERT(expectedPrefix.append("\0\x1", 2));
+  Buffer encodedPrefix;
+  MOZ_RELEASE_ASSERT(encodedPrefix.
+    append(encoded.begin(), encoded.begin() + 2));
+  EXPECT_EQ(expectedPrefix, encodedPrefix);
+
+  // Next is the issuer key (32 bytes).
+  Buffer encodedKeyHash;
+  MOZ_RELEASE_ASSERT(encodedKeyHash.
+    append(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.length());
+
+  const uint8_t EXPECTED_BYTES[] = {
+    0x00, // version
+    0x00, // signature type
+    0x00, 0x00, 0x01, 0x39, 0xFE, 0x35, 0x3C, 0xF5, // timestamp
+    0x61, 0x62, 0x63, // log signature
+    0x00, 0x00 // extensions (empty)
+  };
+  Buffer expectedBuffer;
+  MOZ_RELEASE_ASSERT(
+    expectedBuffer.append(EXPECTED_BYTES, sizeof(EXPECTED_BYTES)));
+  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, 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.length());
+  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(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(Result::ERROR_BAD_DER,
+    DecodeSignedCertificateTimestamp(invalidLengthSctReader, sct));
+}
+
+TEST_F(CTSerializationTest, EncodesValidSignedTreeHead)
+{
+  SignedTreeHead signedTreeHead;
+  GetSampleSignedTreeHead(signedTreeHead);
+
+  Buffer encoded;
+  ASSERT_EQ(Success,
+    EncodeTreeHeadSignature(signedTreeHead, encoded));
+  // Expected size is 50 bytes:
+  // Byte 0 is version, byte 1 is signature type
+  // Bytes 2-9 are timestamp
+  // Bytes 10-17 are tree size
+  // Bytes 18-49 are sha256 root hash
+  ASSERT_EQ(50u, encoded.length());
+  const uint8_t EXPECTED_BYTES_PREFIX[] = {
+    0x00, // version
+    0x01, // signature type
+    0x00, 0x00, 0x01, 0x45, 0x3c, 0x5f, 0xb8, 0x35, // timestamp
+    0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x15  // tree size
+    // 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
new file mode 100644
--- /dev/null
+++ b/security/certverifier/tests/gtest/CTTestUtils.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 "CTTestUtils.h"
+
+#include <stdint.h>
+#include <iomanip>
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Vector.h"
+
+#include "CTSerialization.h"
+#include "pkix/Input.h"
+#include "pkix/Result.h"
+#include "SignedCertificateTimestamp.h"
+#include "SignedTreeHead.h"
+
+namespace mozilla { namespace ct {
+
+using namespace mozilla::pkix;
+
+// The following test vectors are from
+// 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";
+
+// For the sample STH
+const char kSampleSTHSHA256RootHash[] =
+    "726467216167397babca293dca398e4ce6b621b18b9bc42f30c900d1f92ac1e4";
+const char kSampleSTHTreeHeadSignature[] =
+    "0403004730450220365a91a2a88f2b9332f41d8959fa7086da7e6d634b7b089bc9da066426"
+    "6c7a20022100e38464f3c0fd066257b982074f7ac87655e0c8f714768a050b4be9a7b441cb"
+    "d3";
+const size_t kSampleSTHTreeSize = 21u;
+const uint64_t kSampleSTHTimestamp = 1396877277237u;
+
+
+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;
+  }
+  MOZ_RELEASE_ASSERT(false);
+}
+
+static Buffer
+HexToBytes(const char* hexData)
+{
+  size_t hexLen = strlen(hexData);
+  MOZ_RELEASE_ASSERT(hexLen > 0 && (hexLen % 2 == 0));
+  size_t resultLen = hexLen / 2;
+  Buffer result;
+  MOZ_RELEASE_ASSERT(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.infallibleAppend((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;
+  MOZ_RELEASE_ASSERT(result.append(encoded.begin() + 4, encoded.end()));
+  return result;
+}
+
+Buffer
+GetTestSignedCertificateTimestamp()
+{
+  return HexToBytes(kTestSignedCertificateTimestamp);
+}
+
+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);
+}
+
+// A sample, valid STH
+void
+GetSampleSignedTreeHead(SignedTreeHead& sth)
+{
+  sth.version = SignedTreeHead::Version::V1;
+  sth.timestamp = kSampleSTHTimestamp;
+  sth.treeSize = kSampleSTHTreeSize;
+  sth.sha256RootHash = GetSampleSTHSHA256RootHash();
+  GetSampleSTHTreeHeadDecodedSignature(sth.signature);
+}
+
+Buffer
+GetSampleSTHSHA256RootHash()
+{
+  return HexToBytes(kSampleSTHSHA256RootHash);
+}
+
+Buffer
+GetSampleSTHTreeHeadSignature()
+{
+  return HexToBytes(kSampleSTHTreeHeadSignature);
+}
+
+void
+GetSampleSTHTreeHeadDecodedSignature(DigitallySigned& signature)
+{
+  Buffer ths = HexToBytes(kSampleSTHTreeHeadSignature);
+  Input thsInput;
+  MOZ_RELEASE_ASSERT(thsInput.Init(ths.begin(), ths.length()) == Success);
+  Reader thsReader(thsInput);
+  MOZ_RELEASE_ASSERT(DecodeDigitallySigned(thsReader, signature) == Success);
+  MOZ_RELEASE_ASSERT(thsReader.AtEnd());
+}
+
+Buffer
+cloneBuffer(const Buffer& buffer)
+{
+  Buffer cloned;
+  MOZ_RELEASE_ASSERT(cloned.appendAll(buffer));
+  return cloned;
+}
+
+Input
+InputForBuffer(const Buffer& buffer)
+{
+  Input input;
+  MOZ_RELEASE_ASSERT(Success ==
+    input.Init(buffer.begin(), buffer.length()));
+  return input;
+}
+
+} } // namespace mozilla::ct
+
+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.length(); ++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
new file mode 100644
--- /dev/null
+++ b/security/certverifier/tests/gtest/CTTestUtils.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 CTTestUtils_h
+#define CTTestUtils_h
+
+#include <iostream>
+
+#include "pkix/Input.h"
+#include "SignedCertificateTimestamp.h"
+#include "SignedTreeHead.h"
+
+namespace mozilla { namespace ct {
+
+// 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();
+
+// 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();
+
+// A sample, valid STH.
+void GetSampleSignedTreeHead(SignedTreeHead& sth);
+
+// 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);
+
+// We need this in tests code since mozilla::Vector only allows move assignment.
+Buffer cloneBuffer(const Buffer& buffer);
+
+// Returns Input for the data stored in the buffer, failing assertion on error.
+pkix::Input InputForBuffer(const Buffer& buffer);
+
+} } // namespace mozilla::ct
+
+
+namespace mozilla {
+
+// GTest needs this to be in Buffer's namespace (i.e. in mozilla::Vector's).
+std::ostream& operator<<(std::ostream& stream, const ct::Buffer& buf);
+
+} // namespace mozilla
+
+#endif  // CTTestUtils_h
new file mode 100644
--- /dev/null
+++ b/security/certverifier/tests/gtest/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; c-basic-offset: 4; 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 += [
+    'CTSerializationTest.cpp',
+    'CTTestUtils.cpp',
+]
+
+LOCAL_INCLUDES += [
+    '/security/certverifier',
+    '/security/pkix/include',
+]
+
+FINAL_LIBRARY = 'xul-gtest'