Bug 944175 - preliminary review for a subset of Certificate Transparency auxiliary classes; r?keeler draft
authorSergei Chernov <sergei.cv@ndivi.com>
Thu, 21 Jan 2016 21:13:34 +0200
changeset 324000 f2134521326c66b902b83b15de2dcd6363c7339b
parent 323893 977d78a8dd78afbc0153d37fd9887c3a200dce6a
child 513308 ade3232afc759e03e62485eb708319739fd0e7fe
push id9824
push userbmo:sergei.cv@ndivi.com
push dateThu, 21 Jan 2016 19:33:46 +0000
reviewerskeeler
bugs944175
milestone46.0a1
Bug 944175 - preliminary review for a subset of Certificate Transparency auxiliary classes; r?keeler This is the first set of the base files for CT implementation in Firefox. This commit only contains the base CT definitions and parsing of TLS data, including unit tests. Validation code and similar will follow later. Since the code set is not complete, I think it's best at this stage to only focus on the general issues such as code conventions, base classes chosen, error handling, and so on.
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,474 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+
+#include "CTSerialization.h"
+
+#include <stdint.h>
+#include "mozilla/Assertions.h"
+
+namespace mozilla { namespace ct {
+
+using namespace mozilla::pkix;
+
+namespace {
+
+// Note: length is always specified in bytes.
+// Signed Certificate Timestamp (SCT) Version length
+const size_t kVersionLength = 1;
+
+// Members of a V1 SCT
+const size_t kLogIdLength = 32;
+const size_t kTimestampLength = 8;
+const size_t kExtensionsLengthBytes = 2;
+const size_t kHashAlgorithmLength = 1;
+const size_t kSigAlgorithmLength = 1;
+const size_t kSignatureLengthBytes = 2;
+
+// Members of the digitally-signed struct of a V1 SCT
+const size_t kSignatureTypeLength = 1;
+const size_t kLogEntryTypeLength = 2;
+const size_t kAsn1CertificateLengthBytes = 3;
+const size_t kTbsCertificateLengthBytes = 3;
+
+const size_t kSCTListLengthBytes = 2;
+const size_t kSerializedSCTLengthBytes = 2;
+
+// Members of digitally-signed struct of a STH
+const size_t kTreeSizeLength = 8;
+
+// Length of sha256_root_hash buffer of SignedTreeHead 
+const size_t kSthRootHashLength = 32;
+
+enum SignatureType {
+  SIGNATURE_TYPE_CERTIFICATE_TIMESTAMP = 0,
+  TREE_HASH = 1,
+};
+
+Result BufferToInput(const Buffer& buffer, Input& input) {
+  return input.Init(buffer.begin(), buffer.length());
+}
+
+Result InputToBuffer(Input input, Buffer& buffer) {
+  buffer.clear();
+  if (!buffer.append(input.UnsafeGetData(), input.GetLength())) {
+    return Result::FATAL_ERROR_NO_MEMORY;
+  }
+  return Success;
+}
+
+// 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.
+// |length| indicates the size (in bytes) of the integer.
+template <typename T>
+Result ReadUint(size_t length, Reader& in, T& out) {
+  MOZ_ASSERT(length <= sizeof(T));
+  Result rv;
+  T result = 0;
+  for (size_t i = 0; i < length; ++i) {
+    uint8_t value;
+    rv = in.Read(value);
+    if (rv != Success) {
+      return rv;
+    }
+    result = (result << 8) | value;
+  }
+  out = result;
+  return Success;
+}
+
+// Reads a TLS-encoded field length from |in|.
+// |prefix_length| indicates the bytes needed to represent the length (e.g. 3)
+Result ReadLength(size_t prefix_length, Reader& in, size_t& out) {
+  return ReadUint(prefix_length, in, out);
+}
+
+// Reads |length| bytes from |in|.
+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. |prefix_length| indicates the number of bytes needed to represent
+// the length.
+Result ReadVariableBytes(size_t prefix_length, Reader& in, Input& out) {
+  size_t length;
+  Result rv = ReadLength(prefix_length, in, length);
+  if (rv != Success) {
+    return rv;
+  }
+  return ReadFixedBytes(length, in, out);
+}
+
+// Reads a variable-length list that has been TLS encoded.
+// |max_list_length| contains the overall length of the encoded list.
+// |max_item_length| contains the maximum length of a single item.
+// On success, updates |out| with the encoded list.
+Result ReadList(size_t max_list_length, size_t max_item_length,
+                Reader& in, Vector<Input>& out) {
+  Result rv;
+  Vector<Input> result;
+
+  Input list_data;
+  rv = ReadVariableBytes(max_list_length, in, list_data);
+  if (rv != Success) {
+    return rv;
+  }
+  Reader list_reader(list_data);
+  while (!list_reader.AtEnd()) {
+    Input list_item;
+    rv = ReadVariableBytes(max_item_length, list_reader, list_item);
+    if (rv != Success) {
+      return rv;
+    }
+    if (list_item.GetLength() == 0) {
+      return Result::ERROR_BAD_DER;
+    }
+    if (!result.append(list_item)) {
+      return Result::FATAL_ERROR_NO_MEMORY;
+    }
+  }
+
+  out.swap(result);
+  return Success;
+}
+
+// Checks and converts a hash algorithm.
+// |in| is the numeric representation of the algorithm.
+Result ConvertHashAlgorithm(unsigned in, DigitallySigned::HashAlgorithm& out) {
+  switch (in) {
+    case DigitallySigned::HASH_ALGO_NONE:
+    case DigitallySigned::HASH_ALGO_MD5:
+    case DigitallySigned::HASH_ALGO_SHA1:
+    case DigitallySigned::HASH_ALGO_SHA224:
+    case DigitallySigned::HASH_ALGO_SHA256:
+    case DigitallySigned::HASH_ALGO_SHA384:
+    case DigitallySigned::HASH_ALGO_SHA512:
+      break;
+    default:
+      return Result::ERROR_BAD_DER;
+  }
+  out = static_cast<DigitallySigned::HashAlgorithm>(in);
+  return Success;
+}
+
+// Checks and converts a signing algorithm.
+// |in| is the numeric representation of the algorithm.
+Result ConvertSignatureAlgorithm(
+    unsigned in,
+    DigitallySigned::SignatureAlgorithm& out) {
+  switch (in) {
+    case DigitallySigned::SIG_ALGO_ANONYMOUS:
+    case DigitallySigned::SIG_ALGO_RSA:
+    case DigitallySigned::SIG_ALGO_DSA:
+    case DigitallySigned::SIG_ALGO_ECDSA:
+      break;
+    default:
+      return Result::ERROR_BAD_DER;
+  }
+  out = static_cast<DigitallySigned::SignatureAlgorithm>(in);
+  return Success;
+}
+
+// Writes a TLS-encoded variable length unsigned integer to |output|.
+// |length| indicates the size (in bytes) of the integer.
+// |value| the value itself to be written.
+template <typename T>
+Result WriteUint(size_t length, T value, Buffer& output) {
+  MOZ_ASSERT(length <= sizeof(T));
+  MOZ_ASSERT(length == sizeof(T) || value >> (length * 8) == 0);
+
+  if (!output.reserve(length + output.length())) {
+    return Result::FATAL_ERROR_NO_MEMORY;
+  }
+  for (; length > 0; --length) {
+    uint8_t next_byte = (value >> ((length - 1)* 8)) & 0xFF;
+    output.infallibleAppend(next_byte);
+  }
+  return Success;
+}
+
+// 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.
+Result WriteEncodedBytes(Input input, Buffer& output) {
+  if (!output.append(input.UnsafeGetData(), input.GetLength())) {
+    return Result::FATAL_ERROR_NO_MEMORY;
+  }
+  return Success;
+}
+
+// Writes a variable-length array to |output|.
+// |prefix_length| indicates the number of bytes needed to represnt the length.
+// |input| is the array itself.
+// Fails if the size of |input| is more than 2^|prefix_length| - 1.
+Result WriteVariableBytes(size_t prefix_length, Input input, Buffer& output) {
+  Result rv;
+  size_t input_size = input.GetLength();
+  size_t max_allowed_input_size =
+      static_cast<size_t>(((1 << (prefix_length * 8)) - 1));
+  if (input_size > max_allowed_input_size) {
+    return Result::FATAL_ERROR_INVALID_ARGS;
+  }
+
+  rv = WriteUint(prefix_length, input_size, output);
+  if (rv != Success) {
+    return rv;
+  }
+  return WriteEncodedBytes(input, output);
+}
+
+// Writes a LogEntry of type X.509 cert to |output|.
+// |input| is the LogEntry containing the certificate.
+Result EncodeAsn1CertLogEntry(const LogEntry& entry, Buffer& output) {
+  Result rv;
+  Input leaf_certificate;
+  rv = BufferToInput(entry.leaf_certificate, leaf_certificate);
+  if (rv != Success) {
+    return rv;
+  }
+  return WriteVariableBytes(kAsn1CertificateLengthBytes,
+                            leaf_certificate, output);
+}
+
+// Writes a LogEntry of type PreCertificate to |output|.
+// |input| is the LogEntry containing the TBSCertificate and issuer key hash.
+Result EncodePrecertLogEntry(const LogEntry& entry, Buffer& output) {
+  Result rv;
+  if (entry.issuer_key_hash.length() != kLogIdLength) {
+    return Result::FATAL_ERROR_INVALID_ARGS;
+  }
+  Input issuer_key_hash;
+  rv = BufferToInput(entry.issuer_key_hash, issuer_key_hash);
+  if (rv != Success) {
+    return rv;
+  }
+  rv = WriteEncodedBytes(issuer_key_hash, output);
+  if (rv != Success) {
+    return rv;
+  }
+  Input tbs_certificate;
+  rv = BufferToInput(entry.tbs_certificate, tbs_certificate);
+  if (rv != Success) {
+    return rv;
+  }
+  return WriteVariableBytes(kTbsCertificateLengthBytes,
+                            tbs_certificate, output);
+}
+
+}  // namespace
+
+Result EncodeDigitallySigned(const DigitallySigned& data, Buffer& output) {
+  Result rv;
+  rv = WriteUint(kHashAlgorithmLength, data.hash_algorithm, output);
+  if (rv != Success) {
+    return rv;
+  }
+  rv = WriteUint(kSigAlgorithmLength, data.signature_algorithm, output);
+  if (rv != Success) {
+    return rv;
+  }
+  Input signature_data;
+  rv = BufferToInput(data.signature_data, signature_data);
+  if (rv != Success) {
+    return rv;
+  }
+  return WriteVariableBytes(kSignatureLengthBytes, signature_data, output);
+}
+
+Result DecodeDigitallySigned(Reader& reader, DigitallySigned& output) {
+  Result rv;
+  unsigned hash_algo;
+  unsigned sig_algo;
+  DigitallySigned result;
+
+  rv = ReadUint(kHashAlgorithmLength, reader, hash_algo);
+  if (rv != Success) {
+    return rv;
+  }
+  rv = ReadUint(kSigAlgorithmLength, reader, sig_algo);
+  if (rv != Success) {
+    return rv;
+  }
+
+  Input signature_data;
+  rv = ReadVariableBytes(kSignatureLengthBytes, reader, signature_data);
+  if (rv != Success) {
+    return rv;
+  }
+  rv = InputToBuffer(signature_data, result.signature_data);
+  if (rv != Success) {
+    return rv;
+  }
+
+  rv = ConvertHashAlgorithm(hash_algo, result.hash_algorithm);
+  if (rv != Success) {
+    return rv;
+  }
+  rv = ConvertSignatureAlgorithm(sig_algo, result.signature_algorithm);
+  if (rv != Success) {
+    return rv;
+  }
+
+  output = Move(result);
+  return Success;
+}
+
+Result EncodeLogEntry(const LogEntry& entry, Buffer& output) {
+  Result rv = WriteUint(kLogEntryTypeLength, entry.type, output);
+  if (rv != Success) {
+    return rv;
+  }
+  switch (entry.type) {
+    case LogEntry::LOG_ENTRY_TYPE_X509:
+      return EncodeAsn1CertLogEntry(entry, output);
+    case LogEntry::LOG_ENTRY_TYPE_PRECERT:
+      return EncodePrecertLogEntry(entry, output);
+  }
+  return Result::ERROR_BAD_DER;
+}
+
+static Result WriteTimeSinceEpoch(uint64_t timestamp, Buffer& output) {
+  return WriteUint(kTimestampLength, timestamp, output);
+}
+
+Result EncodeV1SCTSignedData(uint64_t timestamp,
+                             Input serialized_log_entry,
+                             Input extensions,
+                             Buffer& output) {
+  Result rv;
+  rv = WriteUint(kVersionLength, SignedCertificateTimestamp::SCT_VERSION_1,
+    output);
+  if (rv != Success) {
+    return rv;
+  }
+  rv = WriteUint(kSignatureTypeLength, SIGNATURE_TYPE_CERTIFICATE_TIMESTAMP,
+    output);
+  if (rv != Success) {
+    return rv;
+  }
+  rv = WriteTimeSinceEpoch(timestamp, output);
+  // NOTE: serialized_log_entry must already be serialized and contain the
+  // length as the prefix.
+  rv = WriteEncodedBytes(serialized_log_entry, output);
+  if (rv != Success) {
+    return rv;
+  }
+  return WriteVariableBytes(kExtensionsLengthBytes, extensions, output);
+}
+
+pkix::Result EncodeTreeHeadSignature(const SignedTreeHead& signed_tree_head,
+                                     Buffer& output) {
+  Result rv;
+  rv = WriteUint(kVersionLength, signed_tree_head.version, output);
+  if (rv != Success) {
+    return rv;
+  }
+  rv = WriteUint(kSignatureTypeLength, TREE_HASH, output);
+  if (rv != Success) {
+    return rv;
+  }
+  rv = WriteTimeSinceEpoch(signed_tree_head.timestamp, output);
+  if (rv != Success) {
+    return rv;
+  }
+  rv = WriteUint(kTreeSizeLength, signed_tree_head.tree_size, output);
+  if (rv != Success) {
+    return rv;
+  }
+  if (signed_tree_head.sha256_root_hash.length() != kSthRootHashLength) {
+    return Result::FATAL_ERROR_INVALID_ARGS;
+  }
+  Input sha256_root_hash;
+  rv = BufferToInput(signed_tree_head.sha256_root_hash, sha256_root_hash);
+  if (rv != Success) {
+    return rv;
+  }
+  return WriteEncodedBytes(sha256_root_hash, output);
+}
+
+Result DecodeSCTList(Input input, Vector<Input>& output) {
+  Result rv;
+  Reader reader(input);
+  Vector<Input> result;
+
+  rv = ReadList(kSCTListLengthBytes, kSerializedSCTLengthBytes, reader, result);
+  if (rv != Success) {
+    return rv;
+  }
+  if (!reader.AtEnd() || result.empty()) {
+    return Result::ERROR_BAD_DER;
+  }
+  output.swap(result);
+  return Success;
+}
+
+Result DecodeSignedCertificateTimestamp(Reader& reader,
+                                        SignedCertificateTimestamp& output) {
+  Result rv;
+  SignedCertificateTimestamp result;
+  unsigned version;
+
+  rv = ReadUint(kVersionLength, reader, version);
+  if (rv != Success) {
+    return rv;
+  }
+  if (version != SignedCertificateTimestamp::SCT_VERSION_1) {
+    return Result::ERROR_BAD_DER;
+  }
+
+  result.version = SignedCertificateTimestamp::SCT_VERSION_1;
+  uint64_t timestamp;
+  Input log_id;
+  Input extensions;
+
+  rv = ReadFixedBytes(kLogIdLength, reader, log_id);
+  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(log_id, result.log_id);
+  if (rv != Success) {
+    return rv;
+  }
+  rv = InputToBuffer(extensions, result.extensions);
+  if (rv != Success) {
+    return rv;
+  }
+  result.timestamp = timestamp;
+
+  output = Move(result);
+  return Success;
+}
+
+Result EncodeSCTListForTesting(Input sct, Buffer& output) {
+  Result rv;
+  Buffer encoded_sct;
+  rv = WriteVariableBytes(kSerializedSCTLengthBytes, sct, encoded_sct);
+  if (rv != Success) {
+    return rv;
+  }
+  Input encoded_sct_input;
+  rv = BufferToInput(encoded_sct, encoded_sct_input);
+  if (rv != Success) {
+    return rv;
+  }
+  return WriteVariableBytes(kSCTListLengthBytes, encoded_sct_input, output);
+}
+
+} } // namespace mozilla::ct
new file mode 100644
--- /dev/null
+++ b/security/certverifier/CTSerialization.h
@@ -0,0 +1,68 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+
+#ifndef mozilla_ct__SCTSerialization_h
+#define mozilla_ct__SCTSerialization_h
+
+#include "mozilla/Vector.h"
+#include "pkix/Result.h"
+#include "pkix/Input.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 |input|.
+// 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.
+// |serialized_log_entry| the log entry signed by the SCT.
+// |extensions| CT extensions.
+pkix::Result EncodeV1SCTSignedData(
+    uint64_t timestamp,
+    pkix::Input serialized_log_entry,
+    pkix::Input extensions,
+    Buffer& output);
+
+// Encodes the data signed by a Signed Tree Head (STH) |signed_tree_head| into
+// |output|. The signature included in the |signed_tree_head| can then be
+// verified over these bytes.
+pkix::Result EncodeTreeHeadSignature(
+    const SignedTreeHead& signed_tree_head,
+    Buffer& output);
+
+// Decode a list of Signed Certificate Timestamps
+// (SignedCertificateTimestampList as defined in RFC6962): from a single
+// string in |input| to a vector of individually-encoded SCTs |output|.
+// This list is typically obtained from the CT extension in a certificate.
+// Returns success if the list could be read and decoded successfully, and
+// no excess data is left. Note that the validity of each individual SCT
+// should be checked separately.
+pkix::Result DecodeSCTList(pkix::Input input, Vector<pkix::Input>& output);
+
+// Decodes a single SCT from |input| to |output|.
+pkix::Result DecodeSignedCertificateTimestamp(
+    pkix::Reader& input,
+    SignedCertificateTimestamp& output);
+
+// Writes an SCTList into |output|, containing a single |sct|.
+pkix::Result EncodeSCTListForTesting(pkix::Input sct, Buffer& output);
+
+} } // namespace mozilla::ct
+
+#endif // mozilla_ct__SCTSerialization_h
new file mode 100644
--- /dev/null
+++ b/security/certverifier/SignedCertificateTimestamp.cpp
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+
+#include "SignedCertificateTimestamp.h"
+
+namespace mozilla { namespace ct {
+
+void LogEntry::Reset() {
+  type = LogEntry::LOG_ENTRY_TYPE_X509;
+  leaf_certificate.clear();
+  tbs_certificate.clear();
+}
+
+bool DigitallySigned::SignatureParametersMatch(
+    HashAlgorithm other_hash_algorithm,
+    SignatureAlgorithm other_signature_algorithm) const {
+  return (hash_algorithm == other_hash_algorithm) &&
+         (signature_algorithm == other_signature_algorithm);
+}
+
+} } // namespace mozilla::ct
new file mode 100644
--- /dev/null
+++ b/security/certverifier/SignedCertificateTimestamp.h
@@ -0,0 +1,102 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+
+#ifndef mozilla_ct__SignedCertificateTimestamp_h
+#define mozilla_ct__SignedCertificateTimestamp_h
+
+#include "mozilla/Vector.h"
+
+// Structures related to Certificate Transparency (RFC 6962).
+namespace mozilla { namespace ct {
+
+typedef mozilla::Vector<uint8_t> Buffer;
+
+// LogEntry struct in RFC 6962, Section 3.1.
+struct LogEntry
+{
+
+  // LogEntryType enum in RFC 6962, Section 3.1.
+  enum Type {
+    LOG_ENTRY_TYPE_X509 = 0,
+    LOG_ENTRY_TYPE_PRECERT = 1
+  };
+
+  void Reset();
+
+  Type type;
+
+  // Set if type == LOG_ENTRY_TYPE_X509.
+  Buffer leaf_certificate;
+
+  // Set if type == LOG_ENTRY_TYPE_PRECERT.
+  Buffer issuer_key_hash; // SHA256_LENGTH
+  Buffer tbs_certificate;
+};
+
+// 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 HashAlgorithm {
+    HASH_ALGO_NONE = 0,
+    HASH_ALGO_MD5 = 1,
+    HASH_ALGO_SHA1 = 2,
+    HASH_ALGO_SHA224 = 3,
+    HASH_ALGO_SHA256 = 4,
+    HASH_ALGO_SHA384 = 5,
+    HASH_ALGO_SHA512 = 6,
+  };
+
+  enum SignatureAlgorithm {
+    SIG_ALGO_ANONYMOUS = 0,
+    SIG_ALGO_RSA = 1,
+    SIG_ALGO_DSA = 2,
+    SIG_ALGO_ECDSA = 3
+  };
+
+  // Returns true if |other_hash_algorithm| and |other_signature_algorithm|
+  // match this DigitallySigned hash and signature algorithms.
+  bool SignatureParametersMatch(
+      HashAlgorithm other_hash_algorithm,
+      SignatureAlgorithm other_signature_algorithm) const;
+
+  HashAlgorithm hash_algorithm;
+  SignatureAlgorithm signature_algorithm;
+  // 'signature' field.
+  Buffer signature_data;
+};
+
+// SignedCertificateTimestamp struct in RFC 6962, Section 3.2.
+struct SignedCertificateTimestamp
+{
+  // Version enum in RFC 6962, Section 3.2.
+  enum Version {
+    SCT_VERSION_1 = 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 Origin {
+    SCT_EMBEDDED = 0,
+    SCT_FROM_TLS_EXTENSION = 1,
+    SCT_FROM_OCSP_RESPONSE = 2,
+    SCT_ORIGIN_MAX,
+  };
+
+  Version version;
+  Buffer log_id;
+  uint64_t timestamp;
+  Buffer extensions;
+  DigitallySigned signature;
+  Origin origin;
+  // The log description is not one of the SCT fields, but a user-readable
+  // name defined alongside the log key. It should not participate
+  // in equality checks as the log's description could change while
+  // the SCT would be the same.
+  Buffer log_description;
+};
+
+} } // namespace mozilla::ct
+
+#endif // mozilla_ct__SignedCertificateTimestamp_h
new file mode 100644
--- /dev/null
+++ b/security/certverifier/SignedTreeHead.h
@@ -0,0 +1,28 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+
+#ifndef mozilla_ct__SignedTreeHead_h
+#define mozilla_ct__SignedTreeHead_h
+
+#include "SignedCertificateTimestamp.h"
+
+namespace mozilla { namespace ct {
+
+// Signed Tree Head as defined in section 3.5. of RFC6962.
+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
+  // RFC6962-bis to use separate versions, so using a separate scheme here.
+  enum Version { V1 = 0, };
+
+  Version version;
+  uint64_t timestamp;
+  uint64_t tree_size;
+  Buffer sha256_root_hash;
+  DigitallySigned signature;
+};
+
+} } // namespace mozilla::ct
+
+#endif // mozilla_ct__SignedTreeHead_h
--- a/security/certverifier/moz.build
+++ b/security/certverifier/moz.build
@@ -6,36 +6,42 @@
 
 EXPORTS += [
     'CertVerifier.h',
     'OCSPCache.h',
 ]
 
 UNIFIED_SOURCES += [
     '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,211 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+
+#include "SignedCertificateTimestamp.h"
+#include "SignedTreeHead.h"
+#include "CTSerialization.h"
+#include "CTTestUtils.h"
+
+#include "gtest/gtest.h"
+
+namespace mozilla { namespace ct {
+
+using namespace pkix;
+
+Input InputForBuffer(const Buffer& buffer) {
+  Input input;
+  MOZ_RELEASE_ASSERT(Success ==
+    input.Init(buffer.begin(), buffer.length()));
+  return input;
+}
+
+class CtSerializationTest : public ::testing::Test {
+ public:
+  void SetUp() override {
+    test_digitally_signed_ = GetTestDigitallySigned();
+    // The encoded data contains the signature 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.
+    MOZ_RELEASE_ASSERT(signature_data_.
+      append(test_digitally_signed_.begin() + 4, test_digitally_signed_.end()));
+  }
+
+ protected:
+  Buffer test_digitally_signed_;
+  Buffer signature_data_;
+};
+
+TEST_F(CtSerializationTest, DecodesDigitallySigned) {
+  Input digitally_signed = InputForBuffer(test_digitally_signed_);
+  Reader digitally_signed_reader(digitally_signed);
+
+  DigitallySigned parsed;
+  ASSERT_EQ(Success,
+    DecodeDigitallySigned(digitally_signed_reader, parsed));
+  EXPECT_EQ(
+      DigitallySigned::HASH_ALGO_SHA256,
+      parsed.hash_algorithm);
+  EXPECT_EQ(
+      DigitallySigned::SIG_ALGO_ECDSA,
+      parsed.signature_algorithm);
+
+  ASSERT_EQ(signature_data_, parsed.signature_data);
+}
+
+TEST_F(CtSerializationTest, FailsToDecodePartialDigitallySigned) {
+  Input partial;
+  ASSERT_EQ(Success,
+    partial.Init(test_digitally_signed_.begin(),
+      test_digitally_signed_.length() - 5));
+  Reader partial_reader(partial);
+
+  DigitallySigned parsed;
+
+  ASSERT_NE(Success, DecodeDigitallySigned(partial_reader, parsed));
+}
+
+TEST_F(CtSerializationTest, EncodesDigitallySigned) {
+  DigitallySigned digitally_signed;
+  digitally_signed.hash_algorithm = DigitallySigned::HASH_ALGO_SHA256;
+  digitally_signed.signature_algorithm = DigitallySigned::SIG_ALGO_ECDSA;
+  digitally_signed.signature_data = cloneBuffer(signature_data_);
+
+  Buffer encoded;
+
+  ASSERT_EQ(Success, EncodeDigitallySigned(digitally_signed, encoded));
+  EXPECT_EQ(test_digitally_signed_, 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 expected_prefix;
+  MOZ_RELEASE_ASSERT(expected_prefix.append("\0\0\0\x2\xCE", 5));
+  Buffer encoded_prefix;
+  MOZ_RELEASE_ASSERT(encoded_prefix.
+      append(encoded.begin(), encoded.begin() + 5));
+  EXPECT_EQ(expected_prefix, encoded_prefix);
+}
+
+TEST_F(CtSerializationTest, EncodesV1SCTSignedData) {
+  uint64_t timestamp = 1348589665525;
+  const uint8_t DUMMY_BYTES[] = { 0x61, 0x62, 0x63 }; // abc
+  Input dummy_entry(DUMMY_BYTES);
+  Input empty_extensions;
+  Buffer encoded;
+  ASSERT_EQ(Success, EncodeV1SCTSignedData(
+      timestamp, dummy_entry, empty_extensions, 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, 0x00, // log signature
+    0x00 // extentions (empty)
+  };
+  Buffer expected_buffer;
+  MOZ_RELEASE_ASSERT(expected_buffer.append(EXPECTED_BYTES, 15));
+  EXPECT_EQ(expected_buffer, 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 };
+
+  Input encoded(ENCODED);
+  Vector<Input> decoded;
+
+  ASSERT_EQ(Success, DecodeSCTList(encoded, decoded));
+  ASSERT_TRUE(InputsAreEqual(decoded[0], Input(DECODED_1)));
+  ASSERT_TRUE(InputsAreEqual(decoded[1], Input(DECODED_2)));
+}
+
+TEST_F(CtSerializationTest, FailsDecodingInvalidSCTList) {
+  // A list with one item that's too short
+  const uint8_t ENCODED[] = {
+    0x00, 0x0a, 0x00, 0x03, 0x61, 0x62, 0x63, 0x00, 0x05, 0x64, 0x65, 0x66
+  };
+
+  Input encoded(ENCODED);
+  Vector<Input> decoded;
+
+  ASSERT_NE(Success, DecodeSCTList(encoded, decoded));
+}
+
+TEST_F(CtSerializationTest, DecodesSignedCertificateTimestamp) {
+  Buffer encoded_sct_buffer = GetTestSignedCertificateTimestamp();
+  Input encoded_sct_input = InputForBuffer(encoded_sct_buffer);
+  Reader encoded_sct_reader(encoded_sct_input);
+
+  SignedCertificateTimestamp sct;
+  ASSERT_EQ(Success,
+    DecodeSignedCertificateTimestamp(encoded_sct_reader, sct));
+  EXPECT_EQ(0, sct.version);
+  EXPECT_EQ(GetTestPublicKeyId(), sct.log_id);
+  uint64_t expected_time = 1365181456089;
+  EXPECT_EQ(expected_time, sct.timestamp);
+  // Subtracting 4 bytes for signature data (hash & sig algs),
+  // actual signature data should be 71 bytes.
+  EXPECT_EQ((size_t) 71, sct.signature.signature_data.length());
+  EXPECT_TRUE(sct.extensions.empty());
+}
+
+TEST_F(CtSerializationTest, FailsDecodingInvalidSignedCertificateTimestamp) {
+  SignedCertificateTimestamp sct;
+
+  // Invalid version
+  const uint8_t INVALID_VERSION_BYTES[] = { 0x02, 0x00 };
+  Input invalid_version_sct_input(INVALID_VERSION_BYTES);
+  Reader invalid_version_sct_reader(invalid_version_sct_input);
+  ASSERT_EQ(Result::ERROR_BAD_DER,
+    DecodeSignedCertificateTimestamp(invalid_version_sct_reader, sct));
+
+  // Valid version, invalid length (missing data)
+  const uint8_t INVALID_LENGTH_BYTES[] = { 0x00, 0x0a, 0x0b, 0x0c };
+  Input invalid_length_sct_input(INVALID_LENGTH_BYTES);
+  Reader invalid_length_sct_reader(invalid_length_sct_input);
+  ASSERT_EQ(Result::ERROR_BAD_DER,
+    DecodeSignedCertificateTimestamp(invalid_length_sct_reader, sct));
+}
+
+TEST_F(CtSerializationTest, EncodesValidSignedTreeHead) {
+  SignedTreeHead signed_tree_head;
+  GetSampleSignedTreeHead(signed_tree_head);
+
+  Buffer encoded;
+  ASSERT_EQ(Success,
+    EncodeTreeHeadSignature(signed_tree_head, 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 expected_buffer;
+  MOZ_RELEASE_ASSERT(expected_buffer.append(EXPECTED_BYTES_PREFIX, 18));
+  Buffer hash = GetSampleSTHSHA256RootHash();
+  MOZ_RELEASE_ASSERT(expected_buffer.append(hash.begin(), hash.length()));
+  EXPECT_EQ(expected_buffer, encoded);
+}
+
+} }  // namespace mozilla::ct
new file mode 100644
--- /dev/null
+++ b/security/certverifier/tests/gtest/CTTestUtils.cpp
@@ -0,0 +1,327 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+
+#include <stdint.h>
+#include <iomanip>
+
+#include "mozilla/Vector.h"
+#include "mozilla/Assertions.h"
+#include "pkix/Result.h"
+#include "pkix/Input.h"
+#include "SignedCertificateTimestamp.h"
+#include "SignedTreeHead.h"
+#include "CTSerialization.h"
+
+#include "CTTestUtils.h"
+
+namespace mozilla { namespace ct {
+
+namespace {
+
+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);
+}
+
+Buffer HexToBytes(const char* hex_data) {
+  size_t hex_len = strlen(hex_data);
+  MOZ_RELEASE_ASSERT(hex_len > 0 && (hex_len % 2 == 0));
+  size_t result_len = hex_len / 2;
+  Buffer result;
+  MOZ_RELEASE_ASSERT(result.reserve(result_len));
+  for (size_t i = 0; i < result_len; ++i) {
+    uint8_t hi = CharToByte(hex_data[i*2]);
+    uint8_t lo = CharToByte(hex_data[i*2 + 1]);
+    result.infallibleAppend((hi << 4) | lo);
+  }
+  return result;
+}
+
+// The following test vectors are from
+// http://code.google.com/p/certificate-transparency
+
+const char kDefaultDerCert[] =
+    "308202ca30820233a003020102020106300d06092a864886f70d01010505003055310b3009"
+    "06035504061302474231243022060355040a131b4365727469666963617465205472616e73"
+    "706172656e6379204341310e300c0603550408130557616c65733110300e06035504071307"
+    "4572772057656e301e170d3132303630313030303030305a170d3232303630313030303030"
+    "305a3052310b30090603550406130247423121301f060355040a1318436572746966696361"
+    "7465205472616e73706172656e6379310e300c0603550408130557616c65733110300e0603"
+    "55040713074572772057656e30819f300d06092a864886f70d010101050003818d00308189"
+    "02818100b1fa37936111f8792da2081c3fe41925008531dc7f2c657bd9e1de4704160b4c9f"
+    "19d54ada4470404c1c51341b8f1f7538dddd28d9aca48369fc5646ddcc7617f8168aae5b41"
+    "d43331fca2dadfc804d57208949061f9eef902ca47ce88c644e000f06eeeccabdc9dd2f68a"
+    "22ccb09dc76e0dbc73527765b1a37a8c676253dcc10203010001a381ac3081a9301d060355"
+    "1d0e041604146a0d982a3b62c44b6d2ef4e9bb7a01aa9cb798e2307d0603551d2304763074"
+    "80145f9d880dc873e654d4f80dd8e6b0c124b447c355a159a4573055310b30090603550406"
+    "1302474231243022060355040a131b4365727469666963617465205472616e73706172656e"
+    "6379204341310e300c0603550408130557616c65733110300e060355040713074572772057"
+    "656e82010030090603551d1304023000300d06092a864886f70d010105050003818100171c"
+    "d84aac414a9a030f22aac8f688b081b2709b848b4e5511406cd707fed028597a9faefc2eee"
+    "2978d633aaac14ed3235197da87e0f71b8875f1ac9e78b281749ddedd007e3ecf50645f8cb"
+    "f667256cd6a1647b5e13203bb8582de7d6696f656d1c60b95f456b7fcf338571908f1c6972"
+    "7d24c4fccd249295795814d1dac0e6";
+
+const char kDefaultIssuerKeyHash[] =
+    "02adddca08b8bf9861f035940c940156d8350fdff899a6239c6bd77255b8f8fc";
+
+const char kDefaultDerTbsCert[] =
+    "30820233a003020102020107300d06092a864886f70d01010505003055310b300906035504"
+    "061302474231243022060355040a131b4365727469666963617465205472616e7370617265"
+    "6e6379204341310e300c0603550408130557616c65733110300e0603550407130745727720"
+    "57656e301e170d3132303630313030303030305a170d3232303630313030303030305a3052"
+    "310b30090603550406130247423121301f060355040a131843657274696669636174652054"
+    "72616e73706172656e6379310e300c0603550408130557616c65733110300e060355040713"
+    "074572772057656e30819f300d06092a864886f70d010101050003818d0030818902818100"
+    "beef98e7c26877ae385f75325a0c1d329bedf18faaf4d796bf047eb7e1ce15c95ba2f80ee4"
+    "58bd7db86f8a4b252191a79bd700c38e9c0389b45cd4dc9a120ab21e0cb41cd0e72805a410"
+    "cd9c5bdb5d4927726daf1710f60187377ea25b1a1e39eed0b88119dc154dc68f7da8e30caf"
+    "158a33e6c9509f4a05b01409ff5dd87eb50203010001a381ac3081a9301d0603551d0e0416"
+    "04142031541af25c05ffd8658b6843794f5e9036f7b4307d0603551d230476307480145f9d"
+    "880dc873e654d4f80dd8e6b0c124b447c355a159a4573055310b3009060355040613024742"
+    "31243022060355040a131b4365727469666963617465205472616e73706172656e63792043"
+    "41310e300c0603550408130557616c65733110300e060355040713074572772057656e8201"
+    "0030090603551d1304023000";
+
+const char kTestDigitallySigned[] =
+    "0403004730450220606e10ae5c2d5a1b0aed49dc4937f48de71a4e9784e9c208dfbfe9ef53"
+    "6cf7f2022100beb29c72d7d06d61d06bdb38a069469aa86fe12e18bb7cc45689a2c0187ef5"
+    "a5";
+
+const char kTestSignedCertificateTimestamp[] =
+    "00df1c2ec11500945247a96168325ddc5c7959e8f7c6d388fc002e0bbd3f74d7640000013d"
+    "db27ded900000403004730450220606e10ae5c2d5a1b0aed49dc4937f48de71a4e9784e9c2"
+    "08dfbfe9ef536cf7f2022100beb29c72d7d06d61d06bdb38a069469aa86fe12e18bb7cc456"
+    "89a2c0187ef5a5";
+
+const char kEcP256PublicKey[] =
+    "3059301306072a8648ce3d020106082a8648ce3d0301070342000499783cb14533c0161a5a"
+    "b45bf95d08a29cd0ea8dd4c84274e2be59ad15c676960cf0afa1074a57ac644b23479e5b3f"
+    "b7b245eb4b420ef370210371a944beaceb";
+
+const char kTestKeyId[] =
+    "df1c2ec11500945247a96168325ddc5c7959e8f7c6d388fc002e0bbd3f74d764";
+
+const char kTestSCTSignatureData[] =
+    "30450220606e10ae5c2d5a1b0aed49dc4937f48de71a4e9784e9c208dfbfe9ef536cf7f202"
+    "2100beb29c72d7d06d61d06bdb38a069469aa86fe12e18bb7cc45689a2c0187ef5a5";
+
+const char kTestSCTPrecertSignatureData[] =
+    "30450220482f6751af35dba65436be1fd6640f3dbf9a41429495924530288fa3e5e23e0602"
+    "2100e4edc0db3ac572b1e2f5e8ab6a680653987dcf41027dfeffa105519d89edbf08";
+
+// A well-formed OCSP response with fake SCT contents. Does not come from
+// http://code.google.com/p/certificate-transparency, does not pertain to any
+// of the test certs here, and is only used to test extracting the extension
+// contents from the response.
+const char kFakeOCSPResponse[] =
+    "3082016e0a0100a08201673082016306092b060105050730010104820154308201503081ba"
+    "a21604144edfdf5ff9c90ffacfca66e7fbc436bc39ee3fc7180f3230313030313031303630"
+    "3030305a30818e30818b3049300906052b0e03021a050004141833a1e6a4f09577cca0e64c"
+    "e7d145ca4b93700904144edfdf5ff9c90ffacfca66e7fbc436bc39ee3fc7021001aef99bde"
+    "e0bb58c6f2b816bc3ae02f8000180f32303130303130313036303030305aa011180f323033"
+    "30303130313036303030305aa11830163014060a2b06010401d67902040504060404746573"
+    "74300d06092a864886f70d0101050500038181003586ffcf0794e64eb643d52a3d570a1c93"
+    "836395986a2f792dd4e9c70b05161186c55c1658e0607dc9ec0d0924ac37fb99506c870579"
+    "634be1de62ba2fced5f61f3b428f959fcee9bddf6f268c8e14c14fdf3b447786e638a5c8cc"
+    "b610893df17a60e4cff30f4780aeffe0086ef19910f0d9cd7414bc93d1945686f88ad0a3c3"
+    ;
+
+const char kFakeOCSPResponseCert[] =
+    "3082022930820192a003020102021001aef99bdee0bb58c6f2b816bc3ae02f300d06092a86"
+    "4886f70d01010505003015311330110603550403130a54657374696e67204341301e170d31"
+    "30303130313036303030305a170d3332313230313036303030305a30373112301006035504"
+    "0313093132372e302e302e31310b300906035504061302585831143012060355040a130b54"
+    "657374696e67204f726730819d300d06092a864886f70d010101050003818b003081870281"
+    "8100a71998f2930bfe73d031a87f133d2f378eeeeed52a77e44d0fc9ff6f07ff32cbf3da99"
+    "9de4ed65832afcb0807f98787506539d258a0ce3c2c77967653099a9034a9b115a876c39a8"
+    "c4e4ed4acd0c64095946fb39eeeb47a0704dbb018acf48c3a1c4b895fc409fb4a340a986b1"
+    "afc45519ab9eca47c30185c771c64aa5ecf07d020103a35a3058303a06082b060105050701"
+    "01010100042b3029302706082b06010505073001861b687474703a2f2f3132372e302e302e"
+    "313a35353038312f6f637370301a0603551d200101000410300e300c060a2b06010401d679"
+    "020401300d06092a864886f70d01010505000381810065e04fadd3484197f3412479d917e1"
+    "9d8f7db57b526f2d0e4c046f86cebe643bf568ea0cd6570b228842aa057c6a7c79f209dfcd"
+    "3419a4d93b1ecfb1c0224f33083c7d4da023499fbd00d81d6711ad58ffcf65f1545247fe9d"
+    "83203425fd706b4fc5e797002af3d88151be5901eef56ec30aacdfc404be1bd35865ff1943"
+    "2516";
+
+const char kFakeOCSPResponseIssuerCert[] =
+    "308201d13082013aa003020102020101300d06092a864886f70d0101050500301531133011"
+    "0603550403130a54657374696e67204341301e170d3130303130313036303030305a170d33"
+    "32313230313036303030305a3015311330110603550403130a54657374696e672043413081"
+    "9d300d06092a864886f70d010101050003818b0030818702818100a71998f2930bfe73d031"
+    "a87f133d2f378eeeeed52a77e44d0fc9ff6f07ff32cbf3da999de4ed65832afcb0807f9878"
+    "7506539d258a0ce3c2c77967653099a9034a9b115a876c39a8c4e4ed4acd0c64095946fb39"
+    "eeeb47a0704dbb018acf48c3a1c4b895fc409fb4a340a986b1afc45519ab9eca47c30185c7"
+    "71c64aa5ecf07d020103a333303130120603551d130101ff040830060101ff020100301b06"
+    "03551d200101000411300f300d060b2b06010401d6790201ce0f300d06092a864886f70d01"
+    "01050500038181003f4936f8d00e83fbdde331f2c64335dcf7dec8b1a2597683edeed61af0"
+    "fa862412fad848938fe7ab77f1f9a43671ff6fdb729386e26f49e7aca0c0ea216e5970d933"
+    "3ea1e11df2ccb357a5fed5220f9c6239e8946b9b7517707631d51ab996833d58a022cff5a6"
+    "2169ac9258ec110efee78da9ab4a641e3b3c9ee5e8bd291460";
+
+
+const char kFakeOCSPExtensionValue[] = "74657374";  // "test"
+
+// For the sample STH
+const char kSampleSTHSHA256RootHash[] =
+    "726467216167397babca293dca398e4ce6b621b18b9bc42f30c900d1f92ac1e4";
+const char kSampleSTHTreeHeadSignature[] =
+    "0403004730450220365a91a2a88f2b9332f41d8959fa7086da7e6d634b7b089bc9da066426"
+    "6c7a20022100e38464f3c0fd066257b982074f7ac87655e0c8f714768a050b4be9a7b441cb"
+    "d3";
+size_t kSampleSTHTreeSize = 21u;
+int64_t kSampleSTHTimestamp = 1396877277237u;
+
+}  // namespace
+
+void GetX509CertLogEntry(LogEntry& entry) {
+  entry.Reset();
+  entry.type = ct::LogEntry::LOG_ENTRY_TYPE_X509;
+  entry.leaf_certificate = HexToBytes(kDefaultDerCert);
+}
+
+Buffer GetDerEncodedX509Cert() {
+  return HexToBytes(kDefaultDerCert);
+}
+
+void GetPrecertLogEntry(LogEntry& entry) {
+  entry.Reset();
+  entry.type = ct::LogEntry::LOG_ENTRY_TYPE_PRECERT;
+  entry.issuer_key_hash = HexToBytes(kDefaultIssuerKeyHash);
+  entry.tbs_certificate = HexToBytes(kDefaultDerTbsCert);
+}
+
+Buffer GetTestDigitallySigned() {
+  return HexToBytes(kTestDigitallySigned);
+}
+
+Buffer GetTestSignedCertificateTimestamp() {
+  return HexToBytes(kTestSignedCertificateTimestamp);
+}
+
+Buffer GetTestPublicKey() {
+  return HexToBytes(kEcP256PublicKey);
+}
+
+Buffer GetTestPublicKeyId() {
+  return HexToBytes(kTestKeyId);
+}
+
+void GetX509CertSCT(SignedCertificateTimestamp& sct) {
+  sct.version = ct::SignedCertificateTimestamp::SCT_VERSION_1;
+  sct.log_id = 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.hash_algorithm = ct::DigitallySigned::HASH_ALGO_SHA256;
+  sct.signature.signature_algorithm = ct::DigitallySigned::SIG_ALGO_ECDSA;
+  sct.signature.signature_data = HexToBytes(kTestSCTSignatureData);
+}
+
+void GetPrecertSCT(SignedCertificateTimestamp& sct) {
+  sct.version = ct::SignedCertificateTimestamp::SCT_VERSION_1;
+  sct.log_id = 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.hash_algorithm = ct::DigitallySigned::HASH_ALGO_SHA256;
+  sct.signature.signature_algorithm = ct::DigitallySigned::SIG_ALGO_ECDSA;
+  sct.signature.signature_data = HexToBytes(kTestSCTPrecertSignatureData);
+}
+
+Buffer GetDefaultIssuerKeyHash() {
+  return HexToBytes(kDefaultIssuerKeyHash);
+}
+
+Buffer GetDerEncodedFakeOCSPResponse() {
+  return HexToBytes(kFakeOCSPResponse);
+}
+
+Buffer GetFakeOCSPExtensionValue() {
+  return HexToBytes(kFakeOCSPExtensionValue);
+}
+
+Buffer GetDerEncodedFakeOCSPResponseCert() {
+  return HexToBytes(kFakeOCSPResponseCert);
+}
+
+Buffer GetDerEncodedFakeOCSPResponseIssuerCert() {
+  return HexToBytes(kFakeOCSPResponseIssuerCert);
+}
+
+// A sample, valid STH
+void GetSampleSignedTreeHead(SignedTreeHead& sth) {
+  sth.version = SignedTreeHead::V1;
+  sth.timestamp = kSampleSTHTimestamp;
+  sth.tree_size = kSampleSTHTreeSize;
+  sth.sha256_root_hash = GetSampleSTHSHA256RootHash();
+  GetSampleSTHTreeHeadDecodedSignature(sth.signature);
+}
+
+Buffer GetSampleSTHSHA256RootHash() {
+  return HexToBytes(kSampleSTHSHA256RootHash);
+}
+
+Buffer GetSampleSTHTreeHeadSignature() {
+  return HexToBytes(kSampleSTHTreeHeadSignature);
+}
+
+void GetSampleSTHTreeHeadDecodedSignature(DigitallySigned& signature) {
+  Buffer tree_head_signature = HexToBytes(kSampleSTHTreeHeadSignature);
+  pkix::Input ths_input;
+  MOZ_RELEASE_ASSERT(ths_input.Init(tree_head_signature.begin(),
+    tree_head_signature.length()) == pkix::Success);
+  pkix::Reader ths_reader(ths_input);
+  MOZ_RELEASE_ASSERT(
+    DecodeDigitallySigned(ths_reader, signature) == pkix::Success);
+  MOZ_RELEASE_ASSERT(ths_reader.AtEnd());
+}
+
+Buffer cloneBuffer(const Buffer& buffer) {
+  Buffer cloned_buffer;
+  MOZ_RELEASE_ASSERT(cloned_buffer.append(buffer.begin(), buffer.end()));
+  return cloned_buffer;
+}
+
+} } // namespace mozilla::ct
+
+namespace mozilla {
+
+bool operator==(const ct::Buffer& a, const ct::Buffer& b) {
+  return (a.empty() && b.empty()) ||
+    (a.length() == b.length() && 0 == memcmp(a.begin(), b.begin(), a.length()));
+}
+
+bool operator!=(const ct::Buffer& a, const ct::Buffer& b) {
+  return !(a == b);
+}
+
+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,92 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+
+#ifndef mozilla_ct__SCTestUtils_h
+#define mozilla_ct__SCTestUtils_h
+
+#include <iostream>
+
+#include "mozilla/Vector.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.
+
+// 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 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();
+
+// Fake OCSP response with an embedded SCT list.
+Buffer GetDerEncodedFakeOCSPResponse();
+
+// The SCT list embedded in the response above.
+Buffer GetFakeOCSPExtensionValue();
+
+// The cert the OCSP response is for.
+Buffer GetDerEncodedFakeOCSPResponseCert();
+
+// The issuer of the previous cert.
+Buffer GetDerEncodedFakeOCSPResponseIssuerCert();
+
+// 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);
+
+} } // 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);
+
+// GTest needs this to be in Buffer's namespace.
+std::ostream& operator<<(std::ostream& stream, const ct::Buffer& buf);
+
+} // namespace mozilla
+
+#endif  // mozilla_ct__SCTestUtils_h
new file mode 100644
--- /dev/null
+++ b/security/certverifier/tests/gtest/moz.build
@@ -0,0 +1,19 @@
+# -*- 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',
+]
+
+# include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul-gtest'