Bug 1241574 - Certificate Transparency - base definitions and serialization to/from TLS wire format; r?keeler, Cykesiopka draft
authorSergei Chernov <sergei.cv@ndivi.com>
Mon, 11 Apr 2016 16:17:25 +0300
changeset 368213 972a03707c5009ca604ad232913cc1c9b8c689d8
parent 367838 3780a3a6b83aeda143f9562829c830410a0c961e
child 521211 5e8da6a46b6c52aa2bfbd38762616fd365d020d2
push id18469
push usersergei.cv@ndivi.com
push dateWed, 18 May 2016 11:23:35 +0000
reviewerskeeler, Cykesiopka
bugs1241574
milestone49.0a1
Bug 1241574 - Certificate Transparency - base definitions and serialization to/from TLS wire format; r?keeler, 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'