ct: Add basic data structures and serialization routines.

This is to prepare for the implementation of Certificate Transparency in
conscrypt. These structures are described by RFC6962.

Change-Id: Ic2b53a1ac009d58fc0e6ca23b8d9170d921f715c
diff --git a/src/main/java/org/conscrypt/ct/CTConstants.java b/src/main/java/org/conscrypt/ct/CTConstants.java
new file mode 100644
index 0000000..4f77350
--- /dev/null
+++ b/src/main/java/org/conscrypt/ct/CTConstants.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt.ct;
+
+public class CTConstants {
+    public static final String X509_SCT_LIST_OID = "1.3.6.1.4.1.11129.2.4.2";
+    public static final String OCSP_SCT_LIST_OID = "1.3.6.1.4.1.11129.2.4.5";
+
+    public static final int VERSION_LENGTH = 1;
+    public static final int LOGID_LENGTH = 32;
+    public static final int TIMESTAMP_LENGTH = 8;
+    public static final int EXTENSIONS_LENGTH_BYTES = 2;
+
+    public static final int HASH_ALGORITHM_LENGTH = 1;
+    public static final int SIGNATURE_ALGORITHM_LENGTH = 1;
+    public static final int SIGNATURE_LENGTH_BYTES = 2;
+
+    public static final int SIGNATURE_TYPE_LENGTH = 1;
+    public static final int LOG_ENTRY_TYPE_LENGTH = 2;
+    public static final int CERTIFICATE_LENGTH_BYTES = 3;
+
+    public static final int ISSUER_KEY_HASH_LENGTH = 32;
+}
+
diff --git a/src/main/java/org/conscrypt/ct/CertificateEntry.java b/src/main/java/org/conscrypt/ct/CertificateEntry.java
new file mode 100644
index 0000000..ca7dd01
--- /dev/null
+++ b/src/main/java/org/conscrypt/ct/CertificateEntry.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt.ct;
+
+import org.conscrypt.OpenSSLX509Certificate;
+import java.io.OutputStream;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+
+/**
+ * CertificateEntry structure.
+ * This structure describes part of the data which is signed over in SCTs.
+ * It is not defined by the RFC6962, but it is useful to have.
+ *
+ * It's definition would be :
+ * struct {
+ *     LogEntryType entry_type;
+ *     select(entry_type) {
+ *         case x509_entry: ASN.1Cert;
+ *         case precert_entry: PreCert;
+ *     } signed_entry;
+ * } CertificateEntry;
+ *
+ */
+public class CertificateEntry {
+    public enum LogEntryType {
+        X509_ENTRY,
+        PRECERT_ENTRY
+    }
+
+    private final LogEntryType entryType;
+
+    // Only used when entryType is LOG_ENTRY_TYPE_PRECERT
+    private final byte[] issuerKeyHash;
+
+    /* If entryType == PRECERT_ENTRY, this is the encoded TBS of the precertificate.
+       If entryType == X509_ENTRY, this is the encoded leaf certificate. */
+    private final byte[] certificate;
+
+    private CertificateEntry(LogEntryType entryType, byte[] certificate, byte[] issuerKeyHash) {
+        if (entryType == LogEntryType.PRECERT_ENTRY && issuerKeyHash == null) {
+            throw new IllegalArgumentException("issuerKeyHash missing for precert entry.");
+        } else if (entryType == LogEntryType.X509_ENTRY && issuerKeyHash != null) {
+            throw new IllegalArgumentException("unexpected issuerKeyHash for X509 entry.");
+        }
+        
+        if (issuerKeyHash != null && issuerKeyHash.length != CTConstants.ISSUER_KEY_HASH_LENGTH) {
+            throw new IllegalArgumentException("issuerKeyHash must be 32 bytes long");
+        }
+
+        this.entryType = entryType;
+        this.issuerKeyHash = issuerKeyHash;
+        this.certificate = certificate;
+    }
+
+    /**
+     * @throws IllegalArgumentException if issuerKeyHash isn't 32 bytes
+     */
+    public static CertificateEntry createForPrecertificate(byte[] tbsCertificate, byte[] issuerKeyHash) {
+        return new CertificateEntry(LogEntryType.PRECERT_ENTRY, tbsCertificate, issuerKeyHash);
+    }
+
+    public static CertificateEntry createForPrecertificate(OpenSSLX509Certificate leaf,
+            OpenSSLX509Certificate issuer) throws CertificateException {
+        try {
+            if (!leaf.getNonCriticalExtensionOIDs().contains(CTConstants.X509_SCT_LIST_OID)) {
+                throw new CertificateException("Certificate does not contain embedded signed timestamps");
+            }
+
+            OpenSSLX509Certificate preCert = leaf.withDeletedExtension(CTConstants.X509_SCT_LIST_OID);
+            byte[] tbs = preCert.getTBSCertificate();
+
+            byte[] issuerKey = issuer.getPublicKey().getEncoded();
+            MessageDigest md = MessageDigest.getInstance("SHA-256");
+            md.update(issuerKey);
+            byte[] issuerKeyHash = md.digest();
+
+            return createForPrecertificate(tbs, issuerKeyHash);
+        } catch (NoSuchAlgorithmException e) {
+            // SHA-256 is guaranteed to be available
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static CertificateEntry createForX509Certificate(byte[] x509Certificate) {
+        return new CertificateEntry(LogEntryType.X509_ENTRY, x509Certificate, null);
+    }
+
+    public static CertificateEntry createForX509Certificate(X509Certificate cert)
+            throws CertificateEncodingException {
+        return createForX509Certificate(cert.getEncoded());
+    }
+
+    public LogEntryType getEntryType() {
+        return entryType;
+    }
+    public byte[] getCertificate() {
+        return certificate;
+    }
+    public byte[] getIssuerKeyHash() {
+        return issuerKeyHash;
+    }
+
+    /**
+     * TLS encode the CertificateEntry structure.
+     */
+    public void encode(OutputStream output) throws SerializationException {
+        Serialization.writeNumber(output, entryType.ordinal(), CTConstants.LOG_ENTRY_TYPE_LENGTH);
+        if (entryType == LogEntryType.PRECERT_ENTRY) {
+            Serialization.writeFixedBytes(output, issuerKeyHash);
+        }
+        Serialization.writeVariableBytes(output, certificate, CTConstants.CERTIFICATE_LENGTH_BYTES);
+    }
+}
+
diff --git a/src/main/java/org/conscrypt/ct/DigitallySigned.java b/src/main/java/org/conscrypt/ct/DigitallySigned.java
new file mode 100644
index 0000000..7d470de
--- /dev/null
+++ b/src/main/java/org/conscrypt/ct/DigitallySigned.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt.ct;
+
+import java.io.ByteArrayInputStream;
+import java.io.InputStream;
+
+/**
+ * DigitallySigned structure, as defined by RFC5246 Section 4.7.
+ */
+public class DigitallySigned {
+    public enum HashAlgorithm {
+        NONE,
+        MD5,
+        SHA1,
+        SHA224,
+        SHA256,
+        SHA384,
+        SHA512;
+
+        private static HashAlgorithm[] values = values();
+        public static HashAlgorithm valueOf(int ord) {
+            try {
+                return values[ord];
+            } catch (IndexOutOfBoundsException e) {
+                throw new IllegalArgumentException("Invalid hash algorithm " + ord, e);
+            }
+        }
+    }
+
+    public enum SignatureAlgorithm {
+        ANONYMOUS,
+        RSA,
+        DSA,
+        ECDSA;
+
+        private static SignatureAlgorithm[] values = values();
+        public static SignatureAlgorithm valueOf(int ord) {
+            try {
+                return values[ord];
+            } catch (IndexOutOfBoundsException e) {
+                throw new IllegalArgumentException("Invalid signature algorithm " + ord, e);
+            }
+        }
+    }
+
+    private final HashAlgorithm hashAlgorithm;
+    private final SignatureAlgorithm signatureAlgorithm;
+    private final byte[] signature;
+
+    public DigitallySigned(HashAlgorithm hashAlgorithm,
+                           SignatureAlgorithm signatureAlgorithm,
+                           byte[] signature) {
+        this.hashAlgorithm = hashAlgorithm;
+        this.signatureAlgorithm = signatureAlgorithm;
+        this.signature = signature;
+    }
+
+    public DigitallySigned(int hashAlgorithm,
+                           int signatureAlgorithm,
+                           byte[] signature) {
+        this(
+            HashAlgorithm.valueOf(hashAlgorithm),
+            SignatureAlgorithm.valueOf(signatureAlgorithm),
+            signature
+        );
+    }
+
+    public HashAlgorithm getHashAlgorithm() {
+        return hashAlgorithm;
+    }
+    public SignatureAlgorithm getSignatureAlgorithm() {
+        return signatureAlgorithm;
+    }
+    public byte[] getSignature() {
+        return signature;
+    }
+
+    /**
+     * Get the name of the hash and signature combination.
+     * The result can be used to as the argument to {@link java.security.Signature#getInstance}.
+     */
+    public String getAlgorithm() {
+        return String.format("%swith%s", hashAlgorithm, signatureAlgorithm);
+    }
+
+    /**
+     * Decode a TLS encoded DigitallySigned structure.
+     */
+    public static DigitallySigned decode(InputStream input)
+        throws SerializationException {
+        try {
+            return new DigitallySigned(
+                Serialization.readNumber(input, CTConstants.HASH_ALGORITHM_LENGTH),
+                Serialization.readNumber(input, CTConstants.SIGNATURE_ALGORITHM_LENGTH),
+                Serialization.readVariableBytes(input, CTConstants.SIGNATURE_LENGTH_BYTES)
+            );
+        } catch (IllegalArgumentException e) {
+            throw new SerializationException(e);
+        }
+    }
+
+    /**
+     * Decode a TLS encoded DigitallySigned structure.
+     */
+    public static DigitallySigned decode(byte[] input)
+            throws SerializationException {
+        return decode(new ByteArrayInputStream(input));
+    }
+}
+
+
diff --git a/src/main/java/org/conscrypt/ct/Serialization.java b/src/main/java/org/conscrypt/ct/Serialization.java
new file mode 100644
index 0000000..187eb21
--- /dev/null
+++ b/src/main/java/org/conscrypt/ct/Serialization.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt.ct;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+
+public class Serialization {
+    private Serialization() {}
+
+    /**
+     * Read a variable length vector of variable sized elements as described by RFC5246 section 4.3.
+     * The vector is prefixed by its total length, in bytes and in big endian format,
+     * so is each element contained in the vector.
+     * @param listWidth the width of the vector's length field, in bytes.
+     * @param elemWidth the width of each element's length field, in bytes.
+     * @throws SerializationException if EOF is encountered.
+     */
+    public static byte[][] readList(InputStream input, int listWidth, int elemWidth)
+            throws SerializationException {
+        ArrayList<byte[]> result = new ArrayList();
+        byte[] data = readVariableBytes(input, listWidth);
+        input = new ByteArrayInputStream(data);
+        try {
+            while (input.available() > 0) {
+                result.add(readVariableBytes(input, elemWidth));
+            }
+        } catch (IOException e) {
+            throw new SerializationException(e);
+        }
+        return result.toArray(new byte[result.size()][]);
+    }
+
+    /**
+     * Read a length-prefixed sequence of bytes.
+     * The length must be encoded in big endian format.
+     * @param width the width of the length prefix, in bytes.
+     * @throws SerializationException if EOF is encountered, or if {@code width} is negative or
+     * greater than 4
+     */
+    public static byte[] readVariableBytes(InputStream input, int width)
+            throws SerializationException {
+        int length = readNumber(input, width);
+        return readFixedBytes(input, length);
+    }
+
+    /**
+     * Read a fixed number of bytes from the input stream.
+     * @param length the number of bytes to read.
+     * @throws SerializationException if EOF is encountered.
+     */
+    public static byte[] readFixedBytes(InputStream input, int length)
+            throws SerializationException {
+        try {
+            if (length < 0) {
+                throw new SerializationException("Negative length: " + length);
+            }
+
+            byte[] data = new byte[length];
+            int count = input.read(data);
+            if (count < length) {
+                throw new SerializationException("Premature end of input, expected " + length +
+                                                 " bytes, only read " + count);
+            }
+            return data;
+        } catch (IOException e) {
+            throw new SerializationException(e);
+        }
+    }
+
+    /**
+     * Read a number in big endian format from the input stream.
+     * This methods only supports a width of up to 4 bytes.
+     * @param width the width of the number, in bytes.
+     * @throws SerializationException if EOF is encountered, or if {@code width} is negative or
+     * greater than 4
+     */
+    public static int readNumber(InputStream input, int width) throws SerializationException {
+        if (width > 4 || width < 0) {
+            throw new SerializationException("Invalid width: " + width);
+        }
+
+        int result = 0;
+        for (int i = 0; i < width; i++) {
+            result = (result << 8) | (readByte(input) & 0xFF);
+        }
+
+        return result;
+    }
+
+    /**
+     * Read a number in big endian format from the input stream.
+     * This methods supports a width of up to 8 bytes.
+     * @param width the width of the number, in bytes.
+     * @throws SerializationException if EOF is encountered.
+     * @throws IllegalArgumentException if {@code width} is negative or greater than 8
+     */
+    public static long readLong(InputStream input, int width) throws SerializationException {
+        if (width > 8 || width < 0) {
+            throw new IllegalArgumentException("Invalid width: " + width);
+        }
+
+        long result = 0;
+        for (int i = 0; i < width; i++) {
+            result = (result << 8) | (readByte(input) & 0xFF);
+        }
+
+        return result;
+    }
+
+    /**
+     * Read a single byte from the input stream.
+     * @throws SerializationException if EOF is encountered.
+     */
+    public static byte readByte(InputStream input) throws SerializationException {
+        try {
+            int b = input.read();
+            if (b == -1) {
+                throw new SerializationException("Premature end of input, could not read byte.");
+            }
+            return (byte)b;
+        } catch (IOException e) {
+            throw new SerializationException(e);
+        }
+    }
+
+    /**
+     * Write length prefixed sequence of bytes to the ouput stream.
+     * The length prefix is encoded in big endian order.
+     * @param data the data to be written.
+     * @param width the width of the length prefix, in bytes.
+     * @throws SerializationException if the length of {@code data} is too large to fit in
+     * {@code width} bytes or {@code width} is negative.
+     */
+    public static void writeVariableBytes(OutputStream output, byte[] data, int width)
+            throws SerializationException {
+        writeNumber(output, data.length, width);
+        writeFixedBytes(output, data);
+    }
+
+    /**
+     * Write a fixed number sequence of bytes to the ouput stream.
+     * @param data the data to be written.
+     */
+    public static void writeFixedBytes(OutputStream output, byte[] data)
+            throws SerializationException {
+        try {
+            output.write(data);
+        } catch (IOException e) {
+            throw new SerializationException(e);
+        }
+    }
+
+    /**
+     * Write a number to the output stream.
+     * The number is encoded in big endian order.
+     * @param value the value to be written.
+     * @param width the width of the encoded number, in bytes
+     * @throws SerializationException if the number is too large to fit in {@code width} bytes or
+     * {@code width} is negative.
+     */
+    public static void writeNumber(OutputStream output, long value, int width)
+            throws SerializationException {
+        if (width < 0) {
+            throw new SerializationException("Negative width: " + width);
+        }
+        if (width < 8 && value >= (1L << (8*width))) {
+            throw new SerializationException("Number too large, " + value +
+                                             " does not fit in " + width + " bytes");
+        }
+
+        try {
+            while (width > 0) {
+                long shift = (width - 1) * 8;
+                // Java behaves weirdly if shifting by more than the variable's size
+                if (shift < Long.SIZE) {
+                    output.write((byte)((value >> shift) & 0xFF));
+                } else {
+                    output.write(0);
+                }
+
+                width --;
+            }
+        } catch (IOException e) {
+            throw new SerializationException(e);
+        }
+    }
+}
+
diff --git a/src/main/java/org/conscrypt/ct/SerializationException.java b/src/main/java/org/conscrypt/ct/SerializationException.java
new file mode 100644
index 0000000..2beb6cd
--- /dev/null
+++ b/src/main/java/org/conscrypt/ct/SerializationException.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt.ct;
+
+public class SerializationException extends Exception {
+    public SerializationException() {
+    }
+
+    public SerializationException(String message) {
+        super(message);
+    }
+
+    public SerializationException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public SerializationException(Throwable cause) {
+        super(cause);
+    }
+}
+
diff --git a/src/main/java/org/conscrypt/ct/SignedCertificateTimestamp.java b/src/main/java/org/conscrypt/ct/SignedCertificateTimestamp.java
new file mode 100644
index 0000000..5364e54
--- /dev/null
+++ b/src/main/java/org/conscrypt/ct/SignedCertificateTimestamp.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt.ct;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+
+/**
+ * SignedCertificateTimestamp structure, as defined by RFC6962 Section 3.2.
+ */
+public class SignedCertificateTimestamp {
+    public enum Version {
+        V1
+    };
+
+    public enum SignatureType {
+        CERTIFICATE_TIMESTAMP,
+        TREE_HASH
+    };
+
+    public enum Origin {
+        EMBEDDED,
+        TLS_EXTENSION,
+        OCSP_RESPONSE
+    };
+
+    private final Version version;
+    private final byte[] logId;
+    private final long timestamp;
+    private final byte[] extensions;
+    private final DigitallySigned signature;
+
+    // origin is implied from the SCT's source and is not encoded in it,
+    // and affects the verification process.
+    private final Origin origin;
+
+    public SignedCertificateTimestamp(Version version, byte[] logId,
+                                      long timestamp, byte[] extensions,
+                                      DigitallySigned signature, Origin origin) {
+        this.version = version;
+        this.logId = logId;
+        this.timestamp = timestamp;
+        this.extensions = extensions;
+        this.signature = signature;
+        this.origin = origin;
+    }
+
+    public Version getVersion() {
+        return version;
+    }
+    public byte[] getLogID() {
+        return logId;
+    }
+    public long getTimestamp() {
+        return timestamp;
+    }
+    public byte[] getExtensions() {
+        return extensions;
+    }
+    public DigitallySigned getSignature() {
+        return signature;
+    }
+    public Origin getOrigin() {
+        return origin;
+    }
+
+    /**
+     * Decode a TLS encoded SignedCertificateTimestamp structure.
+     */
+    public static SignedCertificateTimestamp decode(InputStream input, Origin origin)
+            throws SerializationException {
+        int version = Serialization.readNumber(input, CTConstants.VERSION_LENGTH);
+        if (version != Version.V1.ordinal()) {
+            throw new SerializationException("Unsupported SCT version " + version);
+        }
+
+        return new SignedCertificateTimestamp(
+            Version.V1,
+            Serialization.readFixedBytes(input, CTConstants.LOGID_LENGTH),
+            Serialization.readLong(input, CTConstants.TIMESTAMP_LENGTH),
+            Serialization.readVariableBytes(input, CTConstants.EXTENSIONS_LENGTH_BYTES),
+            DigitallySigned.decode(input),
+            origin
+        );
+    }
+
+    /**
+     * Decode a TLS encoded SignedCertificateTimestamp structure.
+     */
+    public static SignedCertificateTimestamp decode(byte[] input, Origin origin)
+            throws SerializationException {
+        return decode(new ByteArrayInputStream(input), origin);
+    }
+
+    /**
+     * TLS encode the signed part of the SCT, as described by RFC6962 section 3.2.
+     */
+    public void encodeTBS(OutputStream output, CertificateEntry certEntry)
+            throws SerializationException {
+        Serialization.writeNumber(output, version.ordinal(), CTConstants.VERSION_LENGTH);
+        Serialization.writeNumber(output, SignatureType.CERTIFICATE_TIMESTAMP.ordinal(),
+                                          CTConstants.SIGNATURE_TYPE_LENGTH);
+        Serialization.writeNumber(output, timestamp, CTConstants.TIMESTAMP_LENGTH);
+        certEntry.encode(output);
+        Serialization.writeVariableBytes(output, extensions, CTConstants.EXTENSIONS_LENGTH_BYTES);
+    }
+
+    /**
+     * TLS encode the signed part of the SCT, as described by RFC6962 section 3.2.
+     */
+    public byte[] encodeTBS(CertificateEntry certEntry)
+            throws SerializationException {
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        encodeTBS(output, certEntry);
+        return output.toByteArray();
+    }
+}
+
diff --git a/src/test/java/org/conscrypt/ct/SerializationTest.java b/src/test/java/org/conscrypt/ct/SerializationTest.java
new file mode 100644
index 0000000..bc1affe
--- /dev/null
+++ b/src/test/java/org/conscrypt/ct/SerializationTest.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.conscrypt.ct;
+
+import junit.framework.TestCase;
+import java.io.ByteArrayOutputStream;
+import java.util.Arrays;
+
+public class SerializationTest extends TestCase {
+    public void test_decode_SignedCertificateTimestamp() throws Exception {
+        byte[] in = new byte[] {
+            0x00,                            // version
+            0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // log id
+            0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+            0x01, 0x02, 0x03, 0x04,          // timestamp
+            0x05, 0x06, 0x07, 0x08,
+            0x00, 0x00,                      // extensions length
+            0x04, 0x03,                      // hash & signature algorithm
+            0x00, 0x04,                      // signature length
+            0x12, 0x34, 0x56, 0x78           // signature
+        };
+
+        SignedCertificateTimestamp sct
+            = SignedCertificateTimestamp.decode(in, SignedCertificateTimestamp.Origin.EMBEDDED);
+
+        assertEquals(SignedCertificateTimestamp.Version.V1, sct.getVersion());
+        assertEquals(0x0102030405060708L, sct.getTimestamp());
+        assertEquals(0, sct.getExtensions().length);
+        assertEquals(DigitallySigned.HashAlgorithm.SHA256,
+                     sct.getSignature().getHashAlgorithm());
+        assertEquals(DigitallySigned.SignatureAlgorithm.ECDSA,
+                     sct.getSignature().getSignatureAlgorithm());
+        assertTrue(Arrays.equals(new byte[] { 0x12, 0x34, 0x56, 0x78},
+                     sct.getSignature().getSignature()));
+        assertEquals(SignedCertificateTimestamp.Origin.EMBEDDED, sct.getOrigin());
+    }
+
+    public void test_decode_invalid_SignedCertificateTimestamp() throws Exception {
+        byte[] sct = new byte[] {
+            0x00,                            // version
+            0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // log id
+            0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+            0x01, 0x02, 0x03, 0x04,          // timestamp
+            0x05, 0x06, 0x07, 0x08,
+            0x00, 0x00,                      // extensions length
+            0x04, 0x03,                      // hash & signature algorithm
+            0x00, 0x04,                      // signature length
+            0x12, 0x34, 0x56, 0x78           // signature
+        };
+
+        // Make sure the original decodes fine
+        SignedCertificateTimestamp.decode(sct, SignedCertificateTimestamp.Origin.EMBEDDED);
+
+        // Perform various modification to it, and make sure it throws an exception on decoding
+        try {
+            byte[] in = sct.clone();
+            in[0] = 1; // Modify version field
+            SignedCertificateTimestamp.decode(in, SignedCertificateTimestamp.Origin.EMBEDDED);
+            fail("SerializationException not thrown on unsupported version");
+        } catch (SerializationException e) {}
+
+        try {
+            byte[] in = sct.clone();
+            in[41] = 1; // Modify extensions lemgth
+            SignedCertificateTimestamp.decode(in, SignedCertificateTimestamp.Origin.EMBEDDED);
+            fail("SerializationException not thrown on invalid extensions length");
+        } catch (SerializationException e) {}
+    }
+
+    public void test_decode_DigitallySigned() throws Exception {
+        byte[] in = new byte[] {
+            0x04, 0x03,            // hash & signature algorithm
+            0x00, 0x04,            // signature length
+            0x12, 0x34, 0x56, 0x78 // signature
+        };
+
+        DigitallySigned dst = DigitallySigned.decode(in);
+        assertEquals(DigitallySigned.HashAlgorithm.SHA256, dst.getHashAlgorithm());
+        assertEquals(DigitallySigned.SignatureAlgorithm.ECDSA, dst.getSignatureAlgorithm());
+        assertEqualByteArrays(new byte[] { 0x12, 0x34, 0x56, 0x78}, dst.getSignature());
+    }
+
+    public void test_decode_invalid_DigitallySigned() throws Exception {
+        try {
+            DigitallySigned.decode(new byte[] {
+                0x07, 0x03,            // hash & signature algorithm
+                0x00, 0x04,            // signature length
+                0x12, 0x34, 0x56, 0x78 // signature
+            });
+            fail("SerializationException not thrown on invalid hash type");
+        } catch (SerializationException e) {}
+
+        try {
+            DigitallySigned.decode(new byte[] {
+                0x04, 0x04,            // hash & signature algorithm
+                0x00, 0x04,            // signature length
+                0x12, 0x34, 0x56, 0x78 // signature
+            });
+            fail("SerializationException not thrown on invalid signature type");
+        } catch (SerializationException e) {}
+
+        try {
+            DigitallySigned.decode(new byte[] {
+                0x07, 0x03,            // hash & signature algorithm
+                0x64, 0x35,            // signature length
+                0x12, 0x34, 0x56, 0x78 // signature
+            });
+            fail("SerializationException not thrown on invalid signature length");
+        } catch (SerializationException e) {}
+
+        try {
+            DigitallySigned.decode(new byte[] {
+                0x07, 0x03,            // hash & signature algorithm
+            });
+            fail("SerializationException not thrown on missing signature");
+        } catch (SerializationException e) {}
+    }
+
+    public void test_encode_CertificateEntry_X509Certificate() throws Exception {
+        // Use a dummy certificate. It doesn't matter, CertificateEntry doesn't care about the contents.
+        CertificateEntry entry = CertificateEntry.createForX509Certificate(new byte[] { 0x12, 0x34, 0x56, 0x78 });
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        entry.encode(output);
+
+        assertEqualByteArrays(new byte[] {
+            0x00, 0x00,            // entry_type
+            0x00, 0x00, 0x04,      // x509_entry length
+            0x12, 0x34, 0x56, 0x78 // x509_entry
+        }, output.toByteArray());
+    }
+
+    public void test_encode_CertificateEntry_PreCertificate() throws Exception {
+        // Use a dummy certificate and issuer key hash. It doesn't matter,
+        // CertificateEntry doesn't care about the contents.
+        CertificateEntry entry = CertificateEntry.createForPrecertificate(new byte[] { 0x12, 0x34, 0x56, 0x78 },
+                                                          new byte[32]);
+
+        ByteArrayOutputStream output = new ByteArrayOutputStream();
+        entry.encode(output);
+
+        assertEqualByteArrays(new byte[] {
+            0x00, 0x01,                      // entry_type
+            0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, // issuer key hash
+            0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+            0x00, 0x00, 0x04,                // precert_entry length
+            0x12, 0x34, 0x56, 0x78           // precert_entry
+        }, output.toByteArray());
+    }
+
+    public static void assertEqualByteArrays(byte[] expected, byte[] actual) {
+        assertEquals(Arrays.toString(expected), Arrays.toString(actual));
+    }
+}
+