src/java.base/share/classes/sun/security/ssl/HandshakeHash.java
branchJDK-8145252-TLS13-branch
changeset 56542 56aaa6cb3693
parent 48358 38493aecb3d1
child 56646 e57205a6e4ee
--- a/src/java.base/share/classes/sun/security/ssl/HandshakeHash.java	Fri May 11 14:55:56 2018 -0700
+++ b/src/java.base/share/classes/sun/security/ssl/HandshakeHash.java	Fri May 11 15:53:12 2018 -0700
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2002, 2017, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2003, 2018, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
@@ -23,514 +23,627 @@
  * questions.
  */
 
-
 package sun.security.ssl;
 
 import java.io.ByteArrayOutputStream;
-import java.security.*;
-import java.util.Locale;
+import java.io.IOException;
 import java.nio.ByteBuffer;
+import java.security.MessageDigest;
+import java.util.Arrays;
+import java.util.LinkedList;
+import javax.crypto.SecretKey;
+import sun.security.util.MessageDigestSpi2;
+
+final class HandshakeHash {
+    private TranscriptHash transcriptHash;
+    private LinkedList<byte[]> reserves;    // one handshake message per entry
+    private boolean hasBeenUsed;
+
+    HandshakeHash() {
+        this.transcriptHash = new CacheOnlyHash();
+        this.reserves = new LinkedList<>();
+        this.hasBeenUsed = false;
+    }
 
-/**
- * Abstraction for the SSL/TLS hash of all handshake messages that is
- * maintained to verify the integrity of the negotiation. Internally,
- * it consists of an MD5 and an SHA1 digest. They are used in the client
- * and server finished messages and in certificate verify messages (if sent).
- *
- * This class transparently deals with cloneable and non-cloneable digests.
- *
- * This class now supports TLS 1.2 also. The key difference for TLS 1.2
- * is that you cannot determine the hash algorithms for CertificateVerify
- * at a early stage. On the other hand, it's simpler than TLS 1.1 (and earlier)
- * that there is no messy MD5+SHA1 digests.
- *
- * You need to obey these conventions when using this class:
- *
- * 1. protocolDetermined(version) should be called when the negotiated
- * protocol version is determined.
- *
- * 2. Before protocolDetermined() is called, only update(), and reset()
- * and setFinishedAlg() can be called.
- *
- * 3. After protocolDetermined() is called, reset() cannot be called.
- *
- * 4. After protocolDetermined() is called, if the version is pre-TLS 1.2,
- * getFinishedHash() cannot be called. Otherwise,
- * getMD5Clone() and getSHAClone() cannot be called.
- *
- * 5. getMD5Clone() and getSHAClone() can only be called after
- * protocolDetermined() is called and version is pre-TLS 1.2.
- *
- * 6. getFinishedHash() can only be called after protocolDetermined()
- * and setFinishedAlg() have been called and the version is TLS 1.2.
- *
- * Suggestion: Call protocolDetermined() and setFinishedAlg()
- * as early as possible.
- *
- * Example:
- * <pre>
- * HandshakeHash hh = new HandshakeHash(...)
- * hh.protocolDetermined(ProtocolVersion.TLS12);
- * hh.update(clientHelloBytes);
- * hh.setFinishedAlg("SHA-256");
- * hh.update(serverHelloBytes);
- * ...
- * hh.update(CertificateVerifyBytes);
- * ...
- * hh.update(finished1);
- * byte[] finDigest1 = hh.getFinishedHash();
- * hh.update(finished2);
- * byte[] finDigest2 = hh.getFinishedHash();
- * </pre>
- */
-final class HandshakeHash {
+    // fix the negotiated protocol version and cipher suite
+    void determine(ProtocolVersion protocolVersion,
+            CipherSuite cipherSuite) {
+        if (!(transcriptHash instanceof CacheOnlyHash)) {
+            throw new IllegalStateException(
+                    "Not expected instance of transcript hash");
+        }
 
-    // Common
-
-    // -1:  unknown
-    //  1:  <=TLS 1.1
-    //  2:  TLS 1.2
-    private int version = -1;
-    private ByteArrayOutputStream data = new ByteArrayOutputStream();
-
-    // For TLS 1.1
-    private MessageDigest md5, sha;
-    private final int clonesNeeded;    // needs to be saved for later use
-
-    // For TLS 1.2
-    private MessageDigest finMD;
-
-    // Cache for input record handshake hash computation
-    private ByteArrayOutputStream reserve = new ByteArrayOutputStream();
+        CacheOnlyHash coh = (CacheOnlyHash)transcriptHash;
+        if (protocolVersion.useTLS13PlusSpec()) {
+            transcriptHash = new T13HandshakeHash(cipherSuite);
+        } else if (protocolVersion.useTLS12PlusSpec()) {
+            transcriptHash = new T12HandshakeHash(cipherSuite);
+        } else if (protocolVersion.useTLS10PlusSpec()) {
+            transcriptHash = new T10HandshakeHash(cipherSuite);
+        } else {
+            transcriptHash = new S30HandshakeHash(cipherSuite);
+        }
 
-    /**
-     * Create a new HandshakeHash. needCertificateVerify indicates whether
-     * a hash for the certificate verify message is required.
-     */
-    HandshakeHash(boolean needCertificateVerify) {
-        // We may rework the code later, but for now we use hard-coded number
-        // of clones if the underlying MessageDigests are not cloneable.
-        //
-        // The number used here is based on the current handshake protocols and
-        // implementation.  It may be changed if the handshake processe gets
-        // changed in the future, for example adding a new extension that
-        // requires handshake hash.  Please be careful about the number of
-        // clones if additional handshak hash is required in the future.
-        //
-        // For the current implementation, the handshake hash is required for
-        // the following items:
-        //     . CertificateVerify handshake message (optional)
-        //     . client Finished handshake message
-        //     . server Finished Handshake message
-        //     . the extended Master Secret extension [RFC 7627]
-        //
-        // Note that a late call to server setNeedClientAuth dose not update
-        // the number of clones.  We may address the issue later.
-        //
-        // Note for safety, we allocate one more clone for the current
-        // implementation.  We may consider it more carefully in the future
-        // for the exact number or rework the code in a different way.
-        clonesNeeded = needCertificateVerify ? 5 : 4;
+        byte[] reserved = coh.baos.toByteArray();
+        if (reserved.length != 0) {
+            transcriptHash.update(reserved, 0, reserved.length);
+        }
     }
 
-    void reserve(ByteBuffer input) {
+    HandshakeHash copy() {
+        if (transcriptHash instanceof CacheOnlyHash) {
+            HandshakeHash result = new HandshakeHash();
+            result.transcriptHash = ((CacheOnlyHash)transcriptHash).copy();
+            result.reserves = new LinkedList<>(reserves);
+            result.hasBeenUsed = hasBeenUsed;
+            return result;
+        } else {
+            throw new IllegalStateException("Hash does not support copying");
+        }
+    }
+
+    void receive(byte[] input) {
+        reserves.add(Arrays.copyOf(input, input.length));
+    }
+
+    void receive(byte[] input, int offset, int length) {
+        reserves.add(Arrays.copyOfRange(input, offset, offset + length));
+    }
+
+    void receive(ByteBuffer input, int length) {
         if (input.hasArray()) {
-            reserve.write(input.array(),
+            int from = input.position() + input.arrayOffset();
+            int to = from + length;
+            reserves.add(Arrays.copyOfRange(input.array(), from, to));
+        } else {
+            int inPos = input.position();
+            byte[] holder = new byte[length];
+            input.get(holder);
+            input.position(inPos);
+            reserves.add(Arrays.copyOf(holder, holder.length));
+        }
+    }
+    void receive(ByteBuffer input) {
+        receive(input, input.remaining());
+    }
+
+    // For HelloRetryRequest only! Please use this method very carefully!
+    void push(byte[] input) {
+        reserves.push(Arrays.copyOf(input, input.length));
+    }
+
+    // For PreSharedKey to modify the state of the PSK binder hash
+    byte[] removeLastReceived() {
+        return reserves.removeLast();
+    }
+
+    void deliver(byte[] input) {
+        update();
+        transcriptHash.update(input, 0, input.length);
+    }
+
+    void deliver(byte[] input, int offset, int length) {
+        update();
+        transcriptHash.update(input, offset, length);
+    }
+
+    void deliver(ByteBuffer input) {
+        update();
+        if (input.hasArray()) {
+            transcriptHash.update(input.array(),
                     input.position() + input.arrayOffset(), input.remaining());
         } else {
             int inPos = input.position();
             byte[] holder = new byte[input.remaining()];
             input.get(holder);
             input.position(inPos);
-            reserve.write(holder, 0, holder.length);
-        }
-    }
-
-    void reserve(byte[] b, int offset, int len) {
-        reserve.write(b, offset, len);
-    }
-
-    void reload() {
-        if (reserve.size() != 0) {
-            byte[] bytes = reserve.toByteArray();
-            reserve.reset();
-            update(bytes, 0, bytes.length);
+            transcriptHash.update(holder, 0, holder.length);
         }
     }
 
-    void update(ByteBuffer input) {
-
-        // reload if there are reserved messages.
-        reload();
-
-        int inPos = input.position();
-        switch (version) {
-            case 1:
-                md5.update(input);
-                input.position(inPos);
-
-                sha.update(input);
-                input.position(inPos);
-
-                break;
-            default:
-                if (finMD != null) {
-                    finMD.update(input);
-                    input.position(inPos);
-                }
-                if (input.hasArray()) {
-                    data.write(input.array(),
-                            inPos + input.arrayOffset(), input.remaining());
-                } else {
-                    byte[] holder = new byte[input.remaining()];
-                    input.get(holder);
-                    input.position(inPos);
-                    data.write(holder, 0, holder.length);
-                }
-                break;
+    // Use one handshake message if it has not been used.
+    void utilize() {
+        if (hasBeenUsed) {
+            return;
+        }
+        if (reserves.size() != 0) {
+            byte[] holder = reserves.remove();
+            transcriptHash.update(holder, 0, holder.length);
+            hasBeenUsed = true;
         }
     }
 
-    void update(byte handshakeType, byte[] handshakeBody) {
-
-        // reload if there are reserved messages.
-        reload();
-
-        switch (version) {
-            case 1:
-                md5.update(handshakeType);
-                sha.update(handshakeType);
-
-                md5.update((byte)((handshakeBody.length >> 16) & 0xFF));
-                sha.update((byte)((handshakeBody.length >> 16) & 0xFF));
-                md5.update((byte)((handshakeBody.length >> 8) & 0xFF));
-                sha.update((byte)((handshakeBody.length >> 8) & 0xFF));
-                md5.update((byte)(handshakeBody.length & 0xFF));
-                sha.update((byte)(handshakeBody.length & 0xFF));
-
-                md5.update(handshakeBody);
-                sha.update(handshakeBody);
-                break;
-            default:
-                if (finMD != null) {
-                    finMD.update(handshakeType);
-                    finMD.update((byte)((handshakeBody.length >> 16) & 0xFF));
-                    finMD.update((byte)((handshakeBody.length >> 8) & 0xFF));
-                    finMD.update((byte)(handshakeBody.length & 0xFF));
-                    finMD.update(handshakeBody);
-                }
-                data.write(handshakeType);
-                data.write((byte)((handshakeBody.length >> 16) & 0xFF));
-                data.write((byte)((handshakeBody.length >> 8) & 0xFF));
-                data.write((byte)(handshakeBody.length & 0xFF));
-                data.write(handshakeBody, 0, handshakeBody.length);
-                break;
+    // Consume one handshake message if it has not been consumed.
+    void consume() {
+        if (hasBeenUsed) {
+            hasBeenUsed = false;
+            return;
+        }
+        if (reserves.size() != 0) {
+            byte[] holder = reserves.remove();
+            transcriptHash.update(holder, 0, holder.length);
         }
     }
 
-    void update(byte[] b, int offset, int len) {
-
-        // reload if there are reserved messages.
-        reload();
+    void update() {
+        while (reserves.size() != 0) {
+            byte[] holder = reserves.remove();
+            transcriptHash.update(holder, 0, holder.length);
+        }
+        hasBeenUsed = false;
+    }
 
-        switch (version) {
-            case 1:
-                md5.update(b, offset, len);
-                sha.update(b, offset, len);
-                break;
-            default:
-                if (finMD != null) {
-                    finMD.update(b, offset, len);
-                }
-                data.write(b, offset, len);
-                break;
-        }
+    byte[] digest() {
+        // Note that the reserve handshake message may be not a part of
+        // the expected digest.
+        return transcriptHash.digest();
+    }
+
+    void finish() {
+        this.transcriptHash = new CacheOnlyHash();
+        this.reserves = new LinkedList<>();
+        this.hasBeenUsed = false;
+    }
+
+    // Optional
+    byte[] archived() {
+        // Note that the reserve handshake message may be not a part of
+        // the expected digest.
+        return transcriptHash.archived();
     }
 
-    /**
-     * Reset the remaining digests. Note this does *not* reset the number of
-     * digest clones that can be obtained. Digests that have already been
-     * cloned and are gone remain gone.
-     */
-    void reset() {
-        if (version != -1) {
-            throw new RuntimeException(
-                    "reset() can be only be called before protocolDetermined");
-        }
-        data.reset();
+    // Optional, TLS 1.0/1.1 only
+    byte[] digest(String algorithm) {
+        T10HandshakeHash hh = (T10HandshakeHash)transcriptHash;
+        return hh.digest(algorithm);
+    }
+
+    // Optional, SSL 3.0 only
+    byte[] digest(String algorithm, SecretKey masterSecret) {
+        S30HandshakeHash hh = (S30HandshakeHash)transcriptHash;
+        return hh.digest(algorithm, masterSecret);
+    }
+
+    // Optional, SSL 3.0 only
+    byte[] digest(boolean useClientLabel, SecretKey masterSecret) {
+        S30HandshakeHash hh = (S30HandshakeHash)transcriptHash;
+        return hh.digest(useClientLabel, masterSecret);
     }
 
-
-    void protocolDetermined(ProtocolVersion pv) {
+    public boolean isHashable(byte handshakeType) {
+        return handshakeType != SSLHandshake.HELLO_REQUEST.id &&
+               handshakeType != SSLHandshake.HELLO_VERIFY_REQUEST.id;
+    }
 
-        // Do not set again, will ignore
-        if (version != -1) {
-            return;
+    interface TranscriptHash {
+        void update(byte[] input, int offset, int length);
+        byte[] digest();
+        byte[] archived();  // optional
+    }
+
+    // For cache only.
+    private static final class CacheOnlyHash implements TranscriptHash {
+        private final ByteArrayOutputStream baos;
+
+        CacheOnlyHash() {
+            this.baos = new ByteArrayOutputStream();
         }
 
-        if (pv.maybeDTLSProtocol()) {
-            version = pv.compareTo(ProtocolVersion.DTLS12) >= 0 ? 2 : 1;
-        } else {
-            version = pv.compareTo(ProtocolVersion.TLS12) >= 0 ? 2 : 1;
+        @Override
+        public void update(byte[] input, int offset, int length) {
+            baos.write(input, offset, length);
         }
-        switch (version) {
-            case 1:
-                // initiate md5, sha and call update on saved array
-                try {
-                    md5 = CloneableDigest.getDigest("MD5", clonesNeeded);
-                    sha = CloneableDigest.getDigest("SHA", clonesNeeded);
-                } catch (NoSuchAlgorithmException e) {
-                    throw new RuntimeException
-                                ("Algorithm MD5 or SHA not available", e);
-                }
-                byte[] bytes = data.toByteArray();
-                update(bytes, 0, bytes.length);
-                break;
-            case 2:
-                break;
+
+        @Override
+        public byte[] digest() {
+            throw new IllegalStateException(
+                    "Not expected call to handshake hash digest");
         }
-    }
-
-    /////////////////////////////////////////////////////////////
-    // Below are old methods for pre-TLS 1.1
-    /////////////////////////////////////////////////////////////
 
-    /**
-     * Return a new MD5 digest updated with all data hashed so far.
-     */
-    MessageDigest getMD5Clone() {
-        if (version != 1) {
-            throw new RuntimeException(
-                    "getMD5Clone() can be only be called for TLS 1.1");
+        @Override
+        public byte[] archived() {
+            return baos.toByteArray();
         }
-        return cloneDigest(md5);
-    }
 
-    /**
-     * Return a new SHA digest updated with all data hashed so far.
-     */
-    MessageDigest getSHAClone() {
-        if (version != 1) {
-            throw new RuntimeException(
-                    "getSHAClone() can be only be called for TLS 1.1");
-        }
-        return cloneDigest(sha);
-    }
-
-    private static MessageDigest cloneDigest(MessageDigest digest) {
-        try {
-            return (MessageDigest)digest.clone();
-        } catch (CloneNotSupportedException e) {
-            // cannot occur for digests generated via CloneableDigest
-            throw new RuntimeException("Could not clone digest", e);
+        CacheOnlyHash copy() {
+            CacheOnlyHash result = new CacheOnlyHash();
+            try {
+                baos.writeTo(result.baos);
+            } catch (IOException ex) {
+                throw new RuntimeException("unable to to clone hash state");
+            }
+            return result;
         }
     }
 
-    /////////////////////////////////////////////////////////////
-    // Below are new methods for TLS 1.2
-    /////////////////////////////////////////////////////////////
+    static final class S30HandshakeHash implements TranscriptHash {
+        static final byte[] MD5_pad1 = genPad(0x36, 48);
+        static final byte[] MD5_pad2 = genPad(0x5c, 48);
+
+        static final byte[] SHA_pad1 = genPad(0x36, 40);
+        static final byte[] SHA_pad2 = genPad(0x5c, 40);
+
+        private static final byte[] SSL_CLIENT = { 0x43, 0x4C, 0x4E, 0x54 };
+        private static final byte[] SSL_SERVER = { 0x53, 0x52, 0x56, 0x52 };
+
+        private final MessageDigest mdMD5;
+        private final MessageDigest mdSHA;
+        private final TranscriptHash md5;
+        private final TranscriptHash sha;
+        private final ByteArrayOutputStream baos;
+
+        S30HandshakeHash(CipherSuite cipherSuite) {
+            this.mdMD5 = JsseJce.getMessageDigest("MD5");
+            this.mdSHA = JsseJce.getMessageDigest("SHA");
+
+            boolean hasArchived = false;
+            if (mdMD5 instanceof Cloneable) {
+                md5 = new CloneableHash(mdMD5);
+            } else {
+                hasArchived = true;
+                md5 = new NonCloneableHash(mdMD5);
+            }
+            if (mdSHA instanceof Cloneable) {
+                sha = new CloneableHash(mdSHA);
+            } else {
+                hasArchived = true;
+                sha = new NonCloneableHash(mdSHA);
+            }
 
-    private static String normalizeAlgName(String alg) {
-        alg = alg.toUpperCase(Locale.US);
-        if (alg.startsWith("SHA")) {
-            if (alg.length() == 3) {
-                return "SHA-1";
+            if (hasArchived) {
+                this.baos = null;
+            } else {
+                this.baos = new ByteArrayOutputStream();
+            }
+        }
+
+        @Override
+        public void update(byte[] input, int offset, int length) {
+            md5.update(input, offset, length);
+            sha.update(input, offset, length);
+            if (baos != null) {
+                baos.write(input, offset, length);
             }
-            if (alg.charAt(3) != '-') {
-                return "SHA-" + alg.substring(3);
+        }
+
+        @Override
+        public byte[] digest() {
+            byte[] digest = new byte[36];
+            System.arraycopy(md5.digest(), 0, digest, 0, 16);
+            System.arraycopy(sha.digest(), 0, digest, 16, 20);
+
+            return digest;
+        }
+
+        @Override
+        public byte[] archived() {
+            if (baos != null) {
+                return baos.toByteArray();
+            } else if (md5 instanceof NonCloneableHash) {
+                return md5.archived();
+            } else {
+                return sha.archived();
             }
         }
-        return alg;
-    }
-    /**
-     * Specifies the hash algorithm used in Finished. This should be called
-     * based in info in ServerHello.
-     * Can be called multiple times.
-     */
-    void setFinishedAlg(String s) {
-        if (s == null) {
-            throw new RuntimeException(
-                    "setFinishedAlg's argument cannot be null");
+
+        byte[] digest(boolean useClientLabel, SecretKey masterSecret) {
+            MessageDigest md5Clone = cloneMd5();
+            MessageDigest shaClone = cloneSha();
+
+            if (useClientLabel) {
+                md5Clone.update(SSL_CLIENT);
+                shaClone.update(SSL_CLIENT);
+            } else {
+                md5Clone.update(SSL_SERVER);
+                shaClone.update(SSL_SERVER);
+            }
+
+            updateDigest(md5Clone, MD5_pad1, MD5_pad2, masterSecret);
+            updateDigest(shaClone, SHA_pad1, SHA_pad2, masterSecret);
+
+            byte[] digest = new byte[36];
+            System.arraycopy(md5Clone.digest(), 0, digest, 0, 16);
+            System.arraycopy(shaClone.digest(), 0, digest, 16, 20);
+
+            return digest;
+        }
+
+        byte[] digest(String algorithm, SecretKey masterSecret) {
+            if ("RSA".equalsIgnoreCase(algorithm)) {
+                MessageDigest md5Clone = cloneMd5();
+                MessageDigest shaClone = cloneSha();
+                updateDigest(md5Clone, MD5_pad1, MD5_pad2, masterSecret);
+                updateDigest(shaClone, SHA_pad1, SHA_pad2, masterSecret);
+
+                byte[] digest = new byte[36];
+                System.arraycopy(md5Clone.digest(), 0, digest, 0, 16);
+                System.arraycopy(shaClone.digest(), 0, digest, 16, 20);
+
+                return digest;
+            } else {
+                MessageDigest shaClone = cloneSha();
+                updateDigest(shaClone, SHA_pad1, SHA_pad2, masterSecret);
+                return shaClone.digest();
+            }
+        }
+
+        private static byte[] genPad(int b, int count) {
+            byte[] padding = new byte[count];
+            Arrays.fill(padding, (byte)b);
+            return padding;
+        }
+
+        private MessageDigest cloneMd5() {
+            MessageDigest md5Clone;
+            if (mdMD5 instanceof Cloneable) {
+                try {
+                    md5Clone = (MessageDigest)mdMD5.clone();
+                } catch (CloneNotSupportedException ex) {   // unlikely
+                    throw new RuntimeException(
+                            "MessageDigest does no support clone operation");
+                }
+            } else {
+                md5Clone = JsseJce.getMessageDigest("MD5");
+                md5Clone.update(md5.archived());
+            }
+
+            return md5Clone;
+        }
+
+        private MessageDigest cloneSha() {
+            MessageDigest shaClone;
+            if (mdSHA instanceof Cloneable) {
+                try {
+                    shaClone = (MessageDigest)mdSHA.clone();
+                } catch (CloneNotSupportedException ex) {   // unlikely
+                    throw new RuntimeException(
+                            "MessageDigest does no support clone operation");
+                }
+            } else {
+                shaClone = JsseJce.getMessageDigest("SHA");
+                shaClone.update(sha.archived());
+            }
+
+            return shaClone;
+        }
+
+        private static void updateDigest(MessageDigest md,
+                byte[] pad1, byte[] pad2, SecretKey masterSecret) {
+            byte[] keyBytes = "RAW".equals(masterSecret.getFormat())
+                            ? masterSecret.getEncoded() : null;
+            if (keyBytes != null) {
+                md.update(keyBytes);
+            } else {
+                digestKey(md, masterSecret);
+            }
+            md.update(pad1);
+            byte[] temp = md.digest();
+
+            if (keyBytes != null) {
+                md.update(keyBytes);
+            } else {
+                digestKey(md, masterSecret);
+            }
+            md.update(pad2);
+            md.update(temp);
         }
 
-        // Can be called multiple times, but only set once
-        if (finMD != null) return;
+        private static void digestKey(MessageDigest md, SecretKey key) {
+            try {
+                if (md instanceof MessageDigestSpi2) {
+                    ((MessageDigestSpi2)md).engineUpdate(key);
+                } else {
+                    throw new Exception(
+                        "Digest does not support implUpdate(SecretKey)");
+                }
+            } catch (Exception e) {
+                throw new RuntimeException(
+                    "Could not obtain encoded key and "
+                    + "MessageDigest cannot digest key", e);
+            }
+        }
+    }
+
+    // TLS 1.0 and TLS 1.1
+    static final class T10HandshakeHash implements TranscriptHash {
+        private final TranscriptHash md5;
+        private final TranscriptHash sha;
+        private final ByteArrayOutputStream baos;
+
+        T10HandshakeHash(CipherSuite cipherSuite) {
+            MessageDigest mdMD5 = JsseJce.getMessageDigest("MD5");
+            MessageDigest mdSHA = JsseJce.getMessageDigest("SHA");
+
+            boolean hasArchived = false;
+            if (mdMD5 instanceof Cloneable) {
+                md5 = new CloneableHash(mdMD5);
+            } else {
+                hasArchived = true;
+                md5 = new NonCloneableHash(mdMD5);
+            }
+            if (mdSHA instanceof Cloneable) {
+                sha = new CloneableHash(mdSHA);
+            } else {
+                hasArchived = true;
+                sha = new NonCloneableHash(mdSHA);
+            }
 
-        try {
-            // See comment in the contructor.
-            finMD = CloneableDigest.getDigest(normalizeAlgName(s), 4);
-        } catch (NoSuchAlgorithmException e) {
-            throw new Error(e);
+            if (hasArchived) {
+                this.baos = null;
+            } else {
+                this.baos = new ByteArrayOutputStream();
+            }
+        }
+
+        @Override
+        public void update(byte[] input, int offset, int length) {
+            md5.update(input, offset, length);
+            sha.update(input, offset, length);
+            if (baos != null) {
+                baos.write(input, offset, length);
+            }
         }
-        finMD.update(data.toByteArray());
+
+        @Override
+        public byte[] digest() {
+            byte[] digest = new byte[36];
+            System.arraycopy(md5.digest(), 0, digest, 0, 16);
+            System.arraycopy(sha.digest(), 0, digest, 16, 20);
+
+            return digest;
+        }
+
+        byte[] digest(String algorithm) {
+            if ("RSA".equalsIgnoreCase(algorithm)) {
+                return digest();
+            } else {
+                return sha.digest();
+            }
+        }
+
+        @Override
+        public byte[] archived() {
+            if (baos != null) {
+                return baos.toByteArray();
+            } else if (md5 instanceof NonCloneableHash) {
+                return md5.archived();
+            } else {
+                return sha.archived();
+            }
+        }
     }
 
-    byte[] getAllHandshakeMessages() {
-        return data.toByteArray();
+    static final class T12HandshakeHash implements TranscriptHash {
+        private final TranscriptHash transcriptHash;
+        private final ByteArrayOutputStream baos;
+
+        T12HandshakeHash(CipherSuite cipherSuite) {
+            MessageDigest md =
+                    JsseJce.getMessageDigest(cipherSuite.hashAlg.name);
+            if (md instanceof Cloneable) {
+                transcriptHash = new CloneableHash(md);
+                this.baos = null;
+            } else {
+                transcriptHash = new NonCloneableHash(md);
+                this.baos = new ByteArrayOutputStream();
+            }
+        }
+
+        @Override
+        public void update(byte[] input, int offset, int length) {
+            transcriptHash.update(input, offset, length);
+            if (baos != null) {
+                baos.write(input, offset, length);
+            }
+        }
+
+        @Override
+        public byte[] digest() {
+            return transcriptHash.digest();
+        }
+
+        @Override
+        public byte[] archived() {
+            if (baos != null) {
+                return baos.toByteArray();
+            } else {
+                return transcriptHash.archived();
+            }
+        }
     }
 
-    /**
-     * Calculates the hash in Finished. Must be called after setFinishedAlg().
-     * This method can be called twice, for Finished messages of the server
-     * side and client side respectively.
-     */
-    byte[] getFinishedHash() {
-        try {
-            return cloneDigest(finMD).digest();
-        } catch (Exception e) {
-            throw new Error("Error during hash calculation", e);
+    static final class T13HandshakeHash implements TranscriptHash {
+        private final TranscriptHash transcriptHash;
+        private final ByteArrayOutputStream baos;
+
+        T13HandshakeHash(CipherSuite cipherSuite) {
+            MessageDigest md =
+                    JsseJce.getMessageDigest(cipherSuite.hashAlg.name);
+            if (md instanceof Cloneable) {
+                transcriptHash = new CloneableHash(md);
+                this.baos = null;
+            } else {
+                transcriptHash = new NonCloneableHash(md);
+                this.baos = new ByteArrayOutputStream();
+            }
+        }
+
+        @Override
+        public void update(byte[] input, int offset, int length) {
+            transcriptHash.update(input, offset, length);
+            if (baos != null) {
+                baos.write(input, offset, length);
+            }
+        }
+
+        @Override
+        public byte[] digest() {
+            return transcriptHash.digest();
+        }
+
+        @Override
+        public byte[] archived() {
+            if (baos != null) {
+                return baos.toByteArray();
+            } else {
+                return transcriptHash.archived();
+            }
+
+            // throw new UnsupportedOperationException("Not supported yet.");
+        }
+    }
+
+    static final class CloneableHash implements TranscriptHash {
+        private final MessageDigest md;
+
+        CloneableHash(MessageDigest md) {
+            this.md = md;
+        }
+
+        @Override
+        public void update(byte[] input, int offset, int length) {
+            md.update(input, offset, length);
+        }
+
+        @Override
+        public byte[] digest() {
+            try {
+                return ((MessageDigest)md.clone()).digest();
+            } catch (CloneNotSupportedException ex) {
+                // unlikely
+                return new byte[0];
+            }
+        }
+
+        @Override
+        public byte[] archived() {
+            throw new UnsupportedOperationException("Not supported yet.");
+        }
+    }
+
+    static final class NonCloneableHash implements TranscriptHash {
+        private final MessageDigest md;
+        private final ByteArrayOutputStream baos = new ByteArrayOutputStream();
+
+        NonCloneableHash(MessageDigest md) {
+            this.md = md;
+        }
+
+        @Override
+        public void update(byte[] input, int offset, int length) {
+            baos.write(input, offset, length);
+        }
+
+        @Override
+        public byte[] digest() {
+            byte[] bytes = baos.toByteArray();
+            md.reset();
+            return md.digest(bytes);
+        }
+
+        @Override
+        public byte[] archived() {
+            return baos.toByteArray();
         }
     }
 }
-
-/**
- * A wrapper for MessageDigests that simulates cloning of non-cloneable
- * digests. It uses the standard MessageDigest API and therefore can be used
- * transparently in place of a regular digest.
- *
- * Note that we extend the MessageDigest class directly rather than
- * MessageDigestSpi. This works because MessageDigest was originally designed
- * this way in the JDK 1.1 days which allows us to avoid creating an internal
- * provider.
- *
- * It can be "cloned" a limited number of times, which is specified at
- * construction time. This is achieved by internally maintaining n digests
- * in parallel. Consequently, it is only 1/n-th times as fast as the original
- * digest.
- *
- * Example:
- *   MessageDigest md = CloneableDigest.getDigest("SHA", 2);
- *   md.update(data1);
- *   MessageDigest md2 = (MessageDigest)md.clone();
- *   md2.update(data2);
- *   byte[] d1 = md2.digest(); // digest of data1 || data2
- *   md.update(data3);
- *   byte[] d2 = md.digest();  // digest of data1 || data3
- *
- * This class is not thread safe.
- *
- */
-final class CloneableDigest extends MessageDigest implements Cloneable {
-
-    /**
-     * The individual MessageDigests. Initially, all elements are non-null.
-     * When clone() is called, the non-null element with the maximum index is
-     * returned and the array element set to null.
-     *
-     * All non-null element are always in the same state.
-     */
-    private final MessageDigest[] digests;
-
-    private CloneableDigest(MessageDigest digest, int n, String algorithm)
-            throws NoSuchAlgorithmException {
-        super(algorithm);
-        digests = new MessageDigest[n];
-        digests[0] = digest;
-        for (int i = 1; i < n; i++) {
-            digests[i] = JsseJce.getMessageDigest(algorithm);
-        }
-    }
-
-    /**
-     * Return a MessageDigest for the given algorithm that can be cloned the
-     * specified number of times. If the default implementation supports
-     * cloning, it is returned. Otherwise, an instance of this class is
-     * returned.
-     */
-    static MessageDigest getDigest(String algorithm, int n)
-            throws NoSuchAlgorithmException {
-        MessageDigest digest = JsseJce.getMessageDigest(algorithm);
-        try {
-            digest.clone();
-            // already cloneable, use it
-            return digest;
-        } catch (CloneNotSupportedException e) {
-            return new CloneableDigest(digest, n, algorithm);
-        }
-    }
-
-    /**
-     * Check if this object is still usable. If it has already been cloned the
-     * maximum number of times, there are no digests left and this object can no
-     * longer be used.
-     */
-    private void checkState() {
-        // XXX handshaking currently doesn't stop updating hashes...
-        // if (digests[0] == null) {
-        //     throw new IllegalStateException("no digests left");
-        // }
-    }
-
-    @Override
-    protected int engineGetDigestLength() {
-        checkState();
-        return digests[0].getDigestLength();
-    }
-
-    @Override
-    protected void engineUpdate(byte b) {
-        checkState();
-        for (int i = 0; (i < digests.length) && (digests[i] != null); i++) {
-            digests[i].update(b);
-        }
-    }
-
-    @Override
-    protected void engineUpdate(byte[] b, int offset, int len) {
-        checkState();
-        for (int i = 0; (i < digests.length) && (digests[i] != null); i++) {
-            digests[i].update(b, offset, len);
-        }
-    }
-
-    @Override
-    protected byte[] engineDigest() {
-        checkState();
-        byte[] digest = digests[0].digest();
-        digestReset();
-        return digest;
-    }
-
-    @Override
-    protected int engineDigest(byte[] buf, int offset, int len)
-            throws DigestException {
-        checkState();
-        int n = digests[0].digest(buf, offset, len);
-        digestReset();
-        return n;
-    }
-
-    /**
-     * Reset all digests after a digest() call. digests[0] has already been
-     * implicitly reset by the digest() call and does not need to be reset
-     * again.
-     */
-    private void digestReset() {
-        for (int i = 1; (i < digests.length) && (digests[i] != null); i++) {
-            digests[i].reset();
-        }
-    }
-
-    @Override
-    protected void engineReset() {
-        checkState();
-        for (int i = 0; (i < digests.length) && (digests[i] != null); i++) {
-            digests[i].reset();
-        }
-    }
-
-    @Override
-    public Object clone() {
-        checkState();
-        for (int i = digests.length - 1; i >= 0; i--) {
-            if (digests[i] != null) {
-                MessageDigest digest = digests[i];
-                digests[i] = null;
-                return digest;
-            }
-        }
-        // cannot occur
-        throw new InternalError();
-    }
-
-}