From 0703319f56ad16f1b0e0632842c41b6a8ebc11e7 Mon Sep 17 00:00:00 2001
From: Thomas Pornin <pornin@bolet.org>
Date: Sun, 30 Jul 2017 23:13:10 +0200
Subject: [PATCH 1/1] Initial commit.

---
 Asn1/AsnElt.cs                     | 2315 +++++++++++++++++
 Asn1/AsnException.cs               |   50 +
 Asn1/AsnIO.cs                      |  402 +++
 Asn1/AsnOID.cs                     |  335 +++
 Asn1/PEMObject.cs                  |   47 +
 Crypto/AES.cs                      |  433 ++++
 Crypto/BigInt.cs                   |  580 +++++
 Crypto/BlockCipherCore.cs          |  222 ++
 Crypto/ChaCha20.cs                 |  311 +++
 Crypto/CryptoException.cs          |   45 +
 Crypto/DES.cs                      |  492 ++++
 Crypto/DSAUtils.cs                 |  322 +++
 Crypto/DigestCore.cs               |  130 +
 Crypto/EC.cs                       |   46 +
 Crypto/ECCurve.cs                  |  235 ++
 Crypto/ECCurve25519.cs             |  248 ++
 Crypto/ECCurvePrime.cs             |  337 +++
 Crypto/ECCurveType.cs              |   42 +
 Crypto/ECDSA.cs                    |  400 +++
 Crypto/ECPrivateKey.cs             |  113 +
 Crypto/ECPublicKey.cs              |  121 +
 Crypto/GHASH.cs                    |  237 ++
 Crypto/HMAC.cs                     |  366 +++
 Crypto/HMAC_DRBG.cs                |  188 ++
 Crypto/IBlockCipher.cs             |  149 ++
 Crypto/IDigest.cs                  |  137 +
 Crypto/IPrivateKey.cs              |   59 +
 Crypto/IPublicKey.cs               |   60 +
 Crypto/MD5.cs                      |  391 +++
 Crypto/ModInt.cs                   | 1122 ++++++++
 Crypto/MutableECPoint.cs           |  252 ++
 Crypto/MutableECPointCurve25519.cs |  286 +++
 Crypto/MutableECPointPrime.cs      |  907 +++++++
 Crypto/NIST.cs                     |  128 +
 Crypto/Poly1305.cs                 |  327 +++
 Crypto/RFC6979.cs                  |  103 +
 Crypto/RNG.cs                      |  224 ++
 Crypto/RSA.cs                      |  560 ++++
 Crypto/RSAPrivateKey.cs            |  312 +++
 Crypto/RSAPublicKey.cs             |  142 +
 Crypto/SHA1.cs                     |  559 ++++
 Crypto/SHA224.cs                   |   73 +
 Crypto/SHA256.cs                   |   73 +
 Crypto/SHA2Big.cs                  |  387 +++
 Crypto/SHA2Small.cs                |  372 +++
 Crypto/SHA384.cs                   |   75 +
 Crypto/SHA512.cs                   |   75 +
 SSLTLS/IO.cs                       |  195 ++
 SSLTLS/IServerChoices.cs           |   66 +
 SSLTLS/IServerPolicy.cs            |   58 +
 SSLTLS/ISessionCache.cs            |   59 +
 SSLTLS/InputRecord.cs              |  187 ++
 SSLTLS/KeyUsage.cs                 |   50 +
 SSLTLS/OutputRecord.cs             |  339 +++
 SSLTLS/PRF.cs                      |  192 ++
 SSLTLS/RecordDecrypt.cs            |   50 +
 SSLTLS/RecordDecryptCBC.cs         |  204 ++
 SSLTLS/RecordDecryptChaPol.cs      |  100 +
 SSLTLS/RecordDecryptGCM.cs         |   86 +
 SSLTLS/RecordDecryptPlain.cs       |   46 +
 SSLTLS/RecordEncrypt.cs            |   49 +
 SSLTLS/RecordEncryptCBC.cs         |  180 ++
 SSLTLS/RecordEncryptChaPol.cs      |  106 +
 SSLTLS/RecordEncryptGCM.cs         |  118 +
 SSLTLS/RecordEncryptPlain.cs       |   53 +
 SSLTLS/SSL.cs                      |  859 +++++++
 SSLTLS/SSLClient.cs                |  905 +++++++
 SSLTLS/SSLEngine.cs                | 1688 ++++++++++++
 SSLTLS/SSLException.cs             |   57 +
 SSLTLS/SSLQuirks.cs                |  197 ++
 SSLTLS/SSLServer.cs                | 1019 ++++++++
 SSLTLS/SSLServerPolicyBasic.cs     |  358 +++
 SSLTLS/SSLSessionCacheLRU.cs       |  119 +
 SSLTLS/SSLSessionParameters.cs     |   88 +
 Tests/Poly1305Ref.cs               |  120 +
 Tests/TestCrypto.cs                | 3843 ++++++++++++++++++++++++++++
 Tests/TestEC.cs                    |  553 ++++
 Tests/TestMath.cs                  |  174 ++
 Twrch/JSON.cs                      |  694 +++++
 Twrch/MergeStream.cs               |  184 ++
 Twrch/Twrch.cs                     | 1336 ++++++++++
 X500/DNPart.cs                     |  550 ++++
 X500/X500Name.cs                   |  343 +++
 XKeys/AlgorithmIdentifier.cs       |  120 +
 XKeys/KF.cs                        |  650 +++++
 ZInt/ZInt.cs                       | 3250 +++++++++++++++++++++++
 build.cmd                          |    5 +
 build.sh                           |   22 +
 conf/bearssl.json                  |  250 ++
 conf/eccert.pem                    |    9 +
 conf/eckey.pem                     |    5 +
 conf/rsacert.pem                   |   16 +
 conf/rsakey.pem                    |   28 +
 93 files changed, 34370 insertions(+)
 create mode 100644 Asn1/AsnElt.cs
 create mode 100644 Asn1/AsnException.cs
 create mode 100644 Asn1/AsnIO.cs
 create mode 100644 Asn1/AsnOID.cs
 create mode 100644 Asn1/PEMObject.cs
 create mode 100644 Crypto/AES.cs
 create mode 100644 Crypto/BigInt.cs
 create mode 100644 Crypto/BlockCipherCore.cs
 create mode 100644 Crypto/ChaCha20.cs
 create mode 100644 Crypto/CryptoException.cs
 create mode 100644 Crypto/DES.cs
 create mode 100644 Crypto/DSAUtils.cs
 create mode 100644 Crypto/DigestCore.cs
 create mode 100644 Crypto/EC.cs
 create mode 100644 Crypto/ECCurve.cs
 create mode 100644 Crypto/ECCurve25519.cs
 create mode 100644 Crypto/ECCurvePrime.cs
 create mode 100644 Crypto/ECCurveType.cs
 create mode 100644 Crypto/ECDSA.cs
 create mode 100644 Crypto/ECPrivateKey.cs
 create mode 100644 Crypto/ECPublicKey.cs
 create mode 100644 Crypto/GHASH.cs
 create mode 100644 Crypto/HMAC.cs
 create mode 100644 Crypto/HMAC_DRBG.cs
 create mode 100644 Crypto/IBlockCipher.cs
 create mode 100644 Crypto/IDigest.cs
 create mode 100644 Crypto/IPrivateKey.cs
 create mode 100644 Crypto/IPublicKey.cs
 create mode 100644 Crypto/MD5.cs
 create mode 100644 Crypto/ModInt.cs
 create mode 100644 Crypto/MutableECPoint.cs
 create mode 100644 Crypto/MutableECPointCurve25519.cs
 create mode 100644 Crypto/MutableECPointPrime.cs
 create mode 100644 Crypto/NIST.cs
 create mode 100644 Crypto/Poly1305.cs
 create mode 100644 Crypto/RFC6979.cs
 create mode 100644 Crypto/RNG.cs
 create mode 100644 Crypto/RSA.cs
 create mode 100644 Crypto/RSAPrivateKey.cs
 create mode 100644 Crypto/RSAPublicKey.cs
 create mode 100644 Crypto/SHA1.cs
 create mode 100644 Crypto/SHA224.cs
 create mode 100644 Crypto/SHA256.cs
 create mode 100644 Crypto/SHA2Big.cs
 create mode 100644 Crypto/SHA2Small.cs
 create mode 100644 Crypto/SHA384.cs
 create mode 100644 Crypto/SHA512.cs
 create mode 100644 SSLTLS/IO.cs
 create mode 100644 SSLTLS/IServerChoices.cs
 create mode 100644 SSLTLS/IServerPolicy.cs
 create mode 100644 SSLTLS/ISessionCache.cs
 create mode 100644 SSLTLS/InputRecord.cs
 create mode 100644 SSLTLS/KeyUsage.cs
 create mode 100644 SSLTLS/OutputRecord.cs
 create mode 100644 SSLTLS/PRF.cs
 create mode 100644 SSLTLS/RecordDecrypt.cs
 create mode 100644 SSLTLS/RecordDecryptCBC.cs
 create mode 100644 SSLTLS/RecordDecryptChaPol.cs
 create mode 100644 SSLTLS/RecordDecryptGCM.cs
 create mode 100644 SSLTLS/RecordDecryptPlain.cs
 create mode 100644 SSLTLS/RecordEncrypt.cs
 create mode 100644 SSLTLS/RecordEncryptCBC.cs
 create mode 100644 SSLTLS/RecordEncryptChaPol.cs
 create mode 100644 SSLTLS/RecordEncryptGCM.cs
 create mode 100644 SSLTLS/RecordEncryptPlain.cs
 create mode 100644 SSLTLS/SSL.cs
 create mode 100644 SSLTLS/SSLClient.cs
 create mode 100644 SSLTLS/SSLEngine.cs
 create mode 100644 SSLTLS/SSLException.cs
 create mode 100644 SSLTLS/SSLQuirks.cs
 create mode 100644 SSLTLS/SSLServer.cs
 create mode 100644 SSLTLS/SSLServerPolicyBasic.cs
 create mode 100644 SSLTLS/SSLSessionCacheLRU.cs
 create mode 100644 SSLTLS/SSLSessionParameters.cs
 create mode 100644 Tests/Poly1305Ref.cs
 create mode 100644 Tests/TestCrypto.cs
 create mode 100644 Tests/TestEC.cs
 create mode 100644 Tests/TestMath.cs
 create mode 100644 Twrch/JSON.cs
 create mode 100644 Twrch/MergeStream.cs
 create mode 100644 Twrch/Twrch.cs
 create mode 100644 X500/DNPart.cs
 create mode 100644 X500/X500Name.cs
 create mode 100644 XKeys/AlgorithmIdentifier.cs
 create mode 100644 XKeys/KF.cs
 create mode 100644 ZInt/ZInt.cs
 create mode 100644 build.cmd
 create mode 100755 build.sh
 create mode 100644 conf/bearssl.json
 create mode 100644 conf/eccert.pem
 create mode 100644 conf/eckey.pem
 create mode 100644 conf/rsacert.pem
 create mode 100644 conf/rsakey.pem

diff --git a/Asn1/AsnElt.cs b/Asn1/AsnElt.cs
new file mode 100644
index 0000000..9c6e1ab
--- /dev/null
+++ b/Asn1/AsnElt.cs
@@ -0,0 +1,2315 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace Asn1 {
+
+/*
+ * An AsnElt instance represents a decoded ASN.1 DER object. It is
+ * immutable.
+ */
+
+public class AsnElt {
+
+	/*
+	 * Universal tag values.
+	 */
+	public const int BOOLEAN           = 1;
+	public const int INTEGER           = 2;
+	public const int BIT_STRING        = 3;
+	public const int OCTET_STRING      = 4;
+	public const int NULL              = 5;
+	public const int OBJECT_IDENTIFIER = 6;
+	public const int Object_Descriptor = 7;
+	public const int EXTERNAL          = 8;
+	public const int REAL              = 9;
+	public const int ENUMERATED        = 10;
+	public const int EMBEDDED_PDV      = 11;
+	public const int UTF8String        = 12;
+	public const int RELATIVE_OID      = 13;
+	public const int SEQUENCE          = 16;
+	public const int SET               = 17;
+	public const int NumericString     = 18;
+	public const int PrintableString   = 19;
+	public const int T61String         = 20;
+	public const int TeletexString     = 20;
+	public const int VideotexString    = 21;
+	public const int IA5String         = 22;
+	public const int UTCTime           = 23;
+	public const int GeneralizedTime   = 24;
+	public const int GraphicString     = 25;
+	public const int VisibleString     = 26;
+	public const int GeneralString     = 27;
+	public const int UniversalString   = 28;
+	public const int CHARACTER_STRING  = 29;
+	public const int BMPString         = 30;
+
+	/*
+	 * Tag classes.
+	 */
+	public const int UNIVERSAL   = 0;
+	public const int APPLICATION = 1;
+	public const int CONTEXT     = 2;
+	public const int PRIVATE     = 3;
+
+	/*
+	 * Internal rules
+	 * ==============
+	 *
+	 * Instances are immutable. They reference an internal buffer
+	 * that they never modify. The buffer is never shown to the
+	 * outside; when decoding and creating, copies are performed
+	 * where necessary.
+	 *
+	 * If the instance was created by decoding, then:
+	 *   objBuf   points to the array containing the complete object
+	 *   objOff   start offset for the object header
+	 *   objLen   complete object length
+	 *   valOff   offset for the first value byte
+	 *   valLen   value length (excluding the null-tag, if applicable)
+	 *   hasEncodedHeader  is true
+	 *
+	 * If the instance was created from an explicit value or from
+	 * sub-elements, then:
+	 *   objBuf   contains the value, or is null
+	 *   objOff   is 0
+	 *   objLen   is -1, or contains the computed object length
+	 *   valOff   is 0
+	 *   valLen   is -1, or contains the computed value length
+	 *   hasEncodedHeader  is false
+	 *
+	 * If objBuf is null, then the object is necessarily constructed
+	 * (Sub is not null). If objBuf is not null, then the encoded
+	 * value is known (the object may be constructed or primitive),
+	 * and valOff/valLen identify the actual value within objBuf.
+	 *
+	 * Tag class and value, and sub-elements, are referenced from
+	 * specific properties.
+	 */
+
+	byte[] objBuf;
+	int objOff;
+	int objLen;
+	int valOff;
+	int valLen;
+	bool hasEncodedHeader;
+
+	AsnElt()
+	{
+	}
+
+	/*
+	 * The tag class for this element.
+	 */
+	int tagClass_;
+	public int TagClass {
+		get {
+			return tagClass_;
+		}
+		private set {
+			tagClass_ = value;
+		}
+	}
+
+	/*
+	 * The tag value for this element.
+	 */
+	int tagValue_;
+	public int TagValue {
+		get {
+			return tagValue_;
+		}
+		private set {
+			tagValue_ = value;
+		}
+	}
+
+	/*
+	 * The sub-elements. This is null if this element is primitive.
+	 * DO NOT MODIFY this array.
+	 */
+	AsnElt[] sub_;
+	public AsnElt[] Sub {
+		get {
+			return sub_;
+		}
+		private set {
+			sub_ = value;
+		}
+	}
+
+	/*
+	 * The "constructed" flag: true for an elements with sub-elements,
+	 * false for a primitive element.
+	 */
+	public bool Constructed {
+		get {
+			return Sub != null;
+		}
+	}
+
+	/*
+	 * The value length. When the object is BER-encoded with an
+	 * indefinite length, the value length includes all the sub-objects
+	 * but NOT the formal null-tag marker.
+	 */
+	public int ValueLength {
+		get {
+			if (valLen < 0) {
+				if (Constructed) {
+					int vlen = 0;
+					foreach (AsnElt a in Sub) {
+						vlen += a.EncodedLength;
+					}
+					valLen = vlen;
+				} else {
+					valLen = objBuf.Length;
+				}
+			}
+			return valLen;
+		}
+	}
+
+	/*
+	 * The encoded object length (complete with header).
+	 */
+	public int EncodedLength {
+		get {
+			if (objLen < 0) {
+				int vlen = ValueLength;
+				objLen = TagLength(TagValue)
+					+ LengthLength(vlen) + vlen;
+			}
+			return objLen;
+		}
+	}
+
+	/*
+	 * Check that this element is constructed. An exception is thrown
+	 * if this is not the case.
+	 */
+	public void CheckConstructed()
+	{
+		if (!Constructed) {
+			throw new AsnException("not constructed");
+		}
+	}
+
+	/*
+	 * Check that this element is primitive. An exception is thrown
+	 * if this is not the case.
+	 */
+	public void CheckPrimitive()
+	{
+		if (Constructed) {
+			throw new AsnException("not primitive");
+		}
+	}
+
+	/*
+	 * Get a sub-element. This method throws appropriate exceptions
+	 * if this element is not constructed, or the requested index
+	 * is out of range.
+	 */
+	public AsnElt GetSub(int n)
+	{
+		CheckConstructed();
+		if (n < 0 || n >= Sub.Length) {
+			throw new AsnException("no such sub-object: n=" + n);
+		}
+		return Sub[n];
+	}
+
+	/*
+	 * Check that the tag is UNIVERSAL with the provided value.
+	 */
+	public void CheckTag(int tv)
+	{
+		CheckTag(UNIVERSAL, tv);
+	}
+
+	/*
+	 * Check that the tag has the specified class and value.
+	 */
+	public void CheckTag(int tc, int tv)
+	{
+		if (TagClass != tc || TagValue != tv) {
+			throw new AsnException("unexpected tag: " + TagString);
+		}
+	}
+
+	/*
+	 * Check that this element is constructed and contains exactly
+	 * 'n' sub-elements.
+	 */
+	public void CheckNumSub(int n)
+	{
+		CheckConstructed();
+		if (Sub.Length != n) {
+			throw new AsnException("wrong number of sub-elements: "
+				+ Sub.Length + " (expected: " + n + ")");
+		}
+	}
+
+	/*
+	 * Check that this element is constructed and contains at least
+	 * 'n' sub-elements.
+	 */
+	public void CheckNumSubMin(int n)
+	{
+		CheckConstructed();
+		if (Sub.Length < n) {
+			throw new AsnException("not enough sub-elements: "
+				+ Sub.Length + " (minimum: " + n + ")");
+		}
+	}
+
+	/*
+	 * Check that this element is constructed and contains no more
+	 * than 'n' sub-elements.
+	 */
+	public void CheckNumSubMax(int n)
+	{
+		CheckConstructed();
+		if (Sub.Length > n) {
+			throw new AsnException("too many sub-elements: "
+				+ Sub.Length + " (maximum: " + n + ")");
+		}
+	}
+
+	/*
+	 * Get a string representation of the tag class and value.
+	 */
+	public string TagString {
+		get {
+			return TagToString(TagClass, TagValue);
+		}
+	}
+
+	static string TagToString(int tc, int tv)
+	{
+		switch (tc) {
+		case UNIVERSAL:
+			break;
+		case APPLICATION:
+			return "APPLICATION:" + tv;
+		case CONTEXT:
+			return "CONTEXT:" + tv;
+		case PRIVATE:
+			return "PRIVATE:" + tv;
+		default:
+			return String.Format("INVALID:{0}/{1}", tc, tv);
+		}
+
+		switch (tv) {
+		case BOOLEAN:           return "BOOLEAN";
+		case INTEGER:           return "INTEGER";
+		case BIT_STRING:        return "BIT_STRING";
+		case OCTET_STRING:      return "OCTET_STRING";
+		case NULL:              return "NULL";
+		case OBJECT_IDENTIFIER: return "OBJECT_IDENTIFIER";
+		case Object_Descriptor: return "Object_Descriptor";
+		case EXTERNAL:          return "EXTERNAL";
+		case REAL:              return "REAL";
+		case ENUMERATED:        return "ENUMERATED";
+		case EMBEDDED_PDV:      return "EMBEDDED_PDV";
+		case UTF8String:        return "UTF8String";
+		case RELATIVE_OID:      return "RELATIVE_OID";
+		case SEQUENCE:          return "SEQUENCE";
+		case SET:               return "SET";
+		case NumericString:     return "NumericString";
+		case PrintableString:   return "PrintableString";
+		case TeletexString:     return "TeletexString";
+		case VideotexString:    return "VideotexString";
+		case IA5String:         return "IA5String";
+		case UTCTime:           return "UTCTime";
+		case GeneralizedTime:   return "GeneralizedTime";
+		case GraphicString:     return "GraphicString";
+		case VisibleString:     return "VisibleString";
+		case GeneralString:     return "GeneralString";
+		case UniversalString:   return "UniversalString";
+		case CHARACTER_STRING:  return "CHARACTER_STRING";
+		case BMPString:         return "BMPString";
+		default:
+			return String.Format("UNIVERSAL:" + tv);
+		}
+	}
+
+	/*
+	 * Get the encoded length for a tag.
+	 */
+	static int TagLength(int tv)
+	{
+		if (tv <= 0x1F) {
+			return 1;
+		}
+		int z = 1;
+		while (tv > 0) {
+			z ++;
+			tv >>= 7;
+		}
+		return z;
+	}
+
+	/*
+	 * Get the encoded length for a length.
+	 */
+	static int LengthLength(int len)
+	{
+		if (len < 0x80) {
+			return 1;
+		}
+		int z = 1;
+		while (len > 0) {
+			z ++;
+			len >>= 8;
+		}
+		return z;
+	}
+
+	/*
+	 * Decode an ASN.1 object. The provided buffer is internally
+	 * copied. Trailing garbage is not tolerated.
+	 */
+	public static AsnElt Decode(byte[] buf)
+	{
+		return Decode(buf, 0, buf.Length, true);
+	}
+
+	/*
+	 * Decode an ASN.1 object. The provided buffer is internally
+	 * copied. Trailing garbage is not tolerated.
+	 */
+	public static AsnElt Decode(byte[] buf, int off, int len)
+	{
+		return Decode(buf, off, len, true);
+	}
+
+	/*
+	 * Decode an ASN.1 object. The provided buffer is internally
+	 * copied. If 'exactLength' is true, then trailing garbage is
+	 * not tolerated (it triggers an exception).
+	 */
+	public static AsnElt Decode(byte[] buf, bool exactLength)
+	{
+		return Decode(buf, 0, buf.Length, exactLength);
+	}
+
+	/*
+	 * Decode an ASN.1 object. The provided buffer is internally
+	 * copied. If 'exactLength' is true, then trailing garbage is
+	 * not tolerated (it triggers an exception).
+	 */
+	public static AsnElt Decode(byte[] buf, int off, int len,
+		bool exactLength)
+	{
+		int tc, tv, valOff, valLen, objLen;
+		bool cons;
+		objLen = Decode(buf, off, len,
+			out tc, out tv, out cons,
+			out valOff, out valLen);
+		if (exactLength && objLen != len) {
+			throw new AsnException("trailing garbage");
+		}
+		byte[] nbuf = new byte[objLen];
+		Array.Copy(buf, off, nbuf, 0, objLen);
+		return DecodeNoCopy(nbuf, 0, objLen);
+	}
+
+	/*
+	 * Internal recursive decoder. The provided array is NOT copied.
+	 * Trailing garbage is ignored (caller should use the 'objLen'
+	 * field to learn the total object length).
+	 */
+	static AsnElt DecodeNoCopy(byte[] buf, int off, int len)
+	{
+		int tc, tv, valOff, valLen, objLen;
+		bool cons;
+		objLen = Decode(buf, off, len,
+			out tc, out tv, out cons,
+			out valOff, out valLen);
+		AsnElt a = new AsnElt();
+		a.TagClass = tc;
+		a.TagValue = tv;
+		a.objBuf = buf;
+		a.objOff = off;
+		a.objLen = objLen;
+		a.valOff = valOff;
+		a.valLen = valLen;
+		a.hasEncodedHeader = true;
+		if (cons) {
+			List<AsnElt> subs = new List<AsnElt>();
+			off = valOff;
+			int lim = valOff + valLen;
+			while (off < lim) {
+				AsnElt b = DecodeNoCopy(buf, off, lim - off);
+				off += b.objLen;
+				subs.Add(b);
+			}
+			a.Sub = subs.ToArray();
+		} else {
+			a.Sub = null;
+		}
+		return a;
+	}
+
+	/*
+	 * Decode the tag and length, and get the value offset and length.
+	 * Returned value if the total object length.
+	 * Note: when an object has indefinite length, the terminated
+	 * "null tag" will NOT be considered part of the "value length".
+	 */
+	static int Decode(byte[] buf, int off, int maxLen,
+		out int tc, out int tv, out bool cons,
+		out int valOff, out int valLen)
+	{
+		int lim = off + maxLen;
+		int orig = off;
+
+		/*
+		 * Decode tag.
+		 */
+		CheckOff(off, lim);
+		tv = buf[off ++];
+		cons = (tv & 0x20) != 0;
+		tc = tv >> 6;
+		tv &= 0x1F;
+		if (tv == 0x1F) {
+			tv = 0;
+			for (;;) {
+				CheckOff(off, lim);
+				int c = buf[off ++];
+				if (tv > 0xFFFFFF) {
+					throw new AsnException(
+						"tag value overflow");
+				}
+				tv = (tv << 7) | (c & 0x7F);
+				if ((c & 0x80) == 0) {
+					break;
+				}
+			}
+		}
+
+		/*
+		 * Decode length.
+		 */
+		CheckOff(off, lim);
+		int vlen = buf[off ++];
+		if (vlen == 0x80) {
+			/*
+			 * Indefinite length. This is not strict DER, but
+			 * we allow it nonetheless; we must check that
+			 * the value was tagged as constructed, though.
+			 */
+			vlen = -1;
+			if (!cons) {
+				throw new AsnException("indefinite length"
+					+ " but not constructed");
+			}
+		} else if (vlen > 0x80) {
+			int lenlen = vlen - 0x80;
+			CheckOff(off + lenlen - 1, lim);
+			vlen = 0;
+			while (lenlen -- > 0) {
+				if (vlen > 0x7FFFFF) {
+					throw new AsnException(
+						"length overflow");
+				}
+				vlen = (vlen << 8) + buf[off ++];
+			}
+		}
+
+		/*
+		 * Length was decoded, so the value starts here.
+		 */
+		valOff = off;
+
+		/*
+		 * If length is indefinite then we must explore sub-objects
+		 * to get the value length.
+		 */
+		if (vlen < 0) {
+			for (;;) {
+				int tc2, tv2, valOff2, valLen2;
+				bool cons2;
+				int slen;
+
+				slen = Decode(buf, off, lim - off,
+					out tc2, out tv2, out cons2,
+					out valOff2, out valLen2);
+				if (tc2 == 0 && tv2 == 0) {
+					if (cons2 || valLen2 != 0) {
+						throw new AsnException(
+							"invalid null tag");
+					}
+					valLen = off - valOff;
+					off += slen;
+					break;
+				} else {
+					off += slen;
+				}
+			}
+		} else {
+			if (vlen > (lim - off)) {
+				throw new AsnException("value overflow");
+			}
+			off += vlen;
+			valLen = off - valOff;
+		}
+
+		return off - orig;
+	}
+
+	static void CheckOff(int off, int lim)
+	{
+		if (off >= lim) {
+			throw new AsnException("offset overflow");
+		}
+	}
+
+	/*
+	 * Get a specific byte from the value. This provided offset is
+	 * relative to the value start (first value byte has offset 0).
+	 */
+	public int ValueByte(int off)
+	{
+		if (off < 0) {
+			throw new AsnException("invalid value offset: " + off);
+		}
+		if (objBuf == null) {
+			int k = 0;
+			foreach (AsnElt a in Sub) {
+				int slen = a.EncodedLength;
+				if ((k + slen) > off) {
+					return a.ValueByte(off - k);
+				}
+			}
+		} else {
+			if (off < valLen) {
+				return objBuf[valOff + off];
+			}
+		}
+		throw new AsnException(String.Format(
+			"invalid value offset {0} (length = {1})",
+			off, ValueLength));
+	}
+
+	/*
+	 * Encode this object into a newly allocated array.
+	 */
+	public byte[] Encode()
+	{
+		byte[] r = new byte[EncodedLength];
+		Encode(r, 0);
+		return r;
+	}
+
+	/*
+	 * Encode this object into the provided array. Encoded object
+	 * length is returned.
+	 */
+	public int Encode(byte[] dst, int off)
+	{
+		return Encode(0, Int32.MaxValue, dst, off);
+	}
+
+	/*
+	 * Encode this object into the provided array. Only bytes
+	 * at offset between 'start' (inclusive) and 'end' (exclusive)
+	 * are actually written. The number of written bytes is returned.
+	 * Offsets are relative to the object start (first tag byte).
+	 */
+	int Encode(int start, int end, byte[] dst, int dstOff)
+	{
+		/*
+		 * If the encoded value is already known, then we just
+		 * dump it.
+		 */
+		if (hasEncodedHeader) {
+			int from = objOff + Math.Max(0, start);
+			int to = objOff + Math.Min(objLen, end);
+			int len = to - from;
+			if (len > 0) {
+				Array.Copy(objBuf, from, dst, dstOff, len);
+				return len;
+			} else {
+				return 0;
+			}
+		}
+
+		int off = 0;
+
+		/*
+		 * Encode tag.
+		 */
+		int fb = (TagClass << 6) + (Constructed ? 0x20 : 0x00);
+		if (TagValue < 0x1F) {
+			fb |= (TagValue & 0x1F);
+			if (start <= off && off < end) {
+				dst[dstOff ++] = (byte)fb;
+			}
+			off ++;
+		} else {
+			fb |= 0x1F;
+			if (start <= off && off < end) {
+				dst[dstOff ++] = (byte)fb;
+			}
+			off ++;
+			int k = 0;
+			for (int v = TagValue; v > 0; v >>= 7, k += 7);
+			while (k > 0) {
+				k -= 7;
+				int v = (TagValue >> k) & 0x7F;
+				if (k != 0) {
+					v |= 0x80;
+				}
+				if (start <= off && off < end) {
+					dst[dstOff ++] = (byte)v;
+				}
+				off ++;
+			}
+		}
+
+		/*
+		 * Encode length.
+		 */
+		int vlen = ValueLength;
+		if (vlen < 0x80) {
+			if (start <= off && off < end) {
+				dst[dstOff ++] = (byte)vlen;
+			}
+			off ++;
+		} else {
+			int k = 0;
+			for (int v = vlen; v > 0; v >>= 8, k += 8);
+			if (start <= off && off < end) {
+				dst[dstOff ++] = (byte)(0x80 + (k >> 3));
+			}
+			off ++;
+			while (k > 0) {
+				k -= 8;
+				if (start <= off && off < end) {
+					dst[dstOff ++] = (byte)(vlen >> k);
+				}
+				off ++;
+			}
+		}
+
+		/*
+		 * Encode value. We must adjust the start/end window to
+		 * make it relative to the value.
+		 */
+		EncodeValue(start - off, end - off, dst, dstOff);
+		off += vlen;
+
+		/*
+		 * Compute copied length.
+		 */
+		return Math.Max(0, Math.Min(off, end) - Math.Max(0, start));
+	}
+
+	/*
+	 * Encode the value into the provided buffer. Only value bytes
+	 * at offsets between 'start' (inclusive) and 'end' (exclusive)
+	 * are written. Actual number of written bytes is returned.
+	 * Offsets are relative to the start of the value.
+	 */
+	int EncodeValue(int start, int end, byte[] dst, int dstOff)
+	{
+		int orig = dstOff;
+		if (objBuf == null) {
+			int k = 0;
+			foreach (AsnElt a in Sub) {
+				int slen = a.EncodedLength;
+				dstOff += a.Encode(start - k, end - k,
+					dst, dstOff);
+				k += slen;
+			}
+		} else {
+			int from = Math.Max(0, start);
+			int to = Math.Min(valLen, end);
+			int len = to - from;
+			if (len > 0) {
+				Array.Copy(objBuf, valOff + from,
+					dst, dstOff, len);
+				dstOff += len;
+			}
+		}
+		return dstOff - orig;
+	}
+
+	/*
+	 * Copy a value chunk. The provided offset ('off') and length ('len')
+	 * define the chunk to copy; the offset is relative to the value
+	 * start (first value byte has offset 0). If the requested window
+	 * exceeds the value boundaries, an exception is thrown.
+	 */
+	public void CopyValueChunk(int off, int len, byte[] dst, int dstOff)
+	{
+		int vlen = ValueLength;
+		if (off < 0 || len < 0 || len > (vlen - off)) {
+			throw new AsnException(String.Format(
+				"invalid value window {0}:{1}"
+				+ " (value length = {2})", off, len, vlen));
+		}
+		EncodeValue(off, off + len, dst, dstOff);
+	}
+
+	/*
+	 * Copy the value into the specified array. The value length is
+	 * returned.
+	 */
+	public int CopyValue(byte[] dst, int off)
+	{
+		return EncodeValue(0, Int32.MaxValue, dst, off);
+	}
+
+	/*
+	 * Get a copy of the value as a freshly allocated array.
+	 */
+	public byte[] CopyValue()
+	{
+		byte[] r = new byte[ValueLength];
+		EncodeValue(0, r.Length, r, 0);
+		return r;
+	}
+
+	/*
+	 * Get the value. This may return a shared buffer, that MUST NOT
+	 * be modified.
+	 */
+	byte[] GetValue(out int off, out int len)
+	{
+		if (objBuf == null) {
+			/*
+			 * We can modify objBuf because CopyValue()
+			 * called ValueLength, thus valLen has been
+			 * filled.
+			 */
+			objBuf = CopyValue();
+			off = 0;
+			len = objBuf.Length;
+		} else {
+			off = valOff;
+			len = valLen;
+		}
+		return objBuf;
+	}
+
+	/*
+	 * Interpret the value as a BOOLEAN.
+	 */
+	public bool GetBoolean()
+	{
+		if (Constructed) {
+			throw new AsnException(
+				"invalid BOOLEAN (constructed)");
+		}
+		int vlen = ValueLength;
+		if (vlen != 1) {
+			throw new AsnException(String.Format(
+				"invalid BOOLEAN (length = {0})", vlen));
+		}
+		return ValueByte(0) != 0;
+	}
+
+	/*
+	 * Interpret the value as an INTEGER. An exception is thrown if
+	 * the value does not fit in a 'long'.
+	 */
+	public long GetInteger()
+	{
+		if (Constructed) {
+			throw new AsnException(
+				"invalid INTEGER (constructed)");
+		}
+		int vlen = ValueLength;
+		if (vlen == 0) {
+			throw new AsnException("invalid INTEGER (length = 0)");
+		}
+		int v = ValueByte(0);
+		long x;
+		if ((v & 0x80) != 0) {
+			x = -1;
+			for (int k = 0; k < vlen; k ++) {
+				if (x < ((-1L) << 55)) {
+					throw new AsnException(
+						"integer overflow (negative)");
+				}
+				x = (x << 8) + (long)ValueByte(k);
+			}
+		} else {
+			x = 0;
+			for (int k = 0; k < vlen; k ++) {
+				if (x >= (1L << 55)) {
+					throw new AsnException(
+						"integer overflow (positive)");
+				}
+				x = (x << 8) + (long)ValueByte(k);
+			}
+		}
+		return x;
+	}
+
+	/*
+	 * Interpret the value as an INTEGER. An exception is thrown if
+	 * the value is outside of the provided range.
+	 */
+	public long GetInteger(long min, long max)
+	{
+		long v = GetInteger();
+		if (v < min || v > max) {
+			throw new AsnException("integer out of allowed range");
+		}
+		return v;
+	}
+
+	/*
+	 * Interpret the value as an INTEGER. Return its hexadecimal
+	 * representation (uppercase), preceded by a '0x' or '-0x'
+	 * header, depending on the integer sign. The number of
+	 * hexadecimal digits is even. Leading zeroes are returned (but
+	 * one may remain, to ensure an even number of digits). If the
+	 * integer has value 0, then 0x00 is returned.
+	 */
+	public string GetIntegerHex()
+	{
+		if (Constructed) {
+			throw new AsnException(
+				"invalid INTEGER (constructed)");
+		}
+		int vlen = ValueLength;
+		if (vlen == 0) {
+			throw new AsnException("invalid INTEGER (length = 0)");
+		}
+		StringBuilder sb = new StringBuilder();
+		byte[] tmp = CopyValue();
+		if (tmp[0] >= 0x80) {
+			sb.Append('-');
+			int cc = 1;
+			for (int i = tmp.Length - 1; i >= 0; i --) {
+				int v = ((~tmp[i]) & 0xFF) + cc;
+				tmp[i] = (byte)v;
+				cc = v >> 8;
+			}
+		}
+		int k = 0;
+		while (k < tmp.Length && tmp[k] == 0) {
+			k ++;
+		}
+		if (k == tmp.Length) {
+			return "0x00";
+		}
+		sb.Append("0x");
+		while (k < tmp.Length) {
+			sb.AppendFormat("{0:X2}", tmp[k ++]);
+		}
+		return sb.ToString();
+	}
+
+	/*
+	 * Interpret the value as an OCTET STRING. The value bytes are
+	 * returned. This method supports constructed values and performs
+	 * the reassembly.
+	 */
+	public byte[] GetOctetString()
+	{
+		int len = GetOctetString(null, 0);
+		byte[] r = new byte[len];
+		GetOctetString(r, 0);
+		return r;
+	}
+
+	/*
+	 * Interpret the value as an OCTET STRING. The value bytes are
+	 * written in dst[], starting at offset 'off', and the total value
+	 * length is returned. If 'dst' is null, then no byte is written
+	 * anywhere, but the total length is still returned. This method
+	 * supports constructed values and performs the reassembly.
+	 */
+	public int GetOctetString(byte[] dst, int off)
+	{
+		if (Constructed) {
+			int orig = off;
+			foreach (AsnElt ae in Sub) {
+				ae.CheckTag(AsnElt.OCTET_STRING);
+				off += ae.GetOctetString(dst, off);
+			}
+			return off - orig;
+		}
+		if (dst != null) {
+			return CopyValue(dst, off);
+		} else {
+			return ValueLength;
+		}
+	}
+
+	/*
+	 * Interpret the value as a BIT STRING. The bits are returned,
+	 * with the "ignored bits" cleared.
+	 */
+	public byte[] GetBitString()
+	{
+		int bitLength;
+		return GetBitString(out bitLength);
+	}
+
+	/*
+	 * Interpret the value as a BIT STRING. The bits are returned,
+	 * with the "ignored bits" cleared. The actual bit length is
+	 * written in 'bitLength'.
+	 */
+	public byte[] GetBitString(out int bitLength)
+	{
+		if (Constructed) {
+			/*
+			 * TODO: support constructed BIT STRING values.
+			 */
+			throw new AsnException(
+				"invalid BIT STRING (constructed)");
+		}
+		int vlen = ValueLength;
+		if (vlen == 0) {
+			throw new AsnException(
+				"invalid BIT STRING (length = 0)");
+		}
+		int fb = ValueByte(0);
+		if (fb > 7 || (vlen == 1 && fb != 0)) {
+			throw new AsnException(String.Format(
+				"invalid BIT STRING (start = 0x{0:X2})", fb));
+		}
+		byte[] r = new byte[vlen - 1];
+		CopyValueChunk(1, vlen - 1, r, 0);
+		if (vlen > 1) {
+			r[r.Length - 1] &= (byte)(0xFF << fb);
+		}
+		bitLength = (r.Length << 3) - fb;
+		return r;
+	}
+
+	/*
+	 * Interpret the value as a NULL.
+	 */
+	public void CheckNull()
+	{
+		if (Constructed) {
+			throw new AsnException(
+				"invalid NULL (constructed)");
+		}
+		if (ValueLength != 0) {
+			throw new AsnException(String.Format(
+				"invalid NULL (length = {0})", ValueLength));
+		}
+	}
+
+	/*
+	 * Interpret the value as an OBJECT IDENTIFIER, and return it
+	 * (in decimal-dotted string format).
+	 */
+	public string GetOID()
+	{
+		CheckPrimitive();
+		if (valLen == 0) {
+			throw new AsnException("zero-length OID");
+		}
+		int v = objBuf[valOff];
+		if (v >= 120) {
+			throw new AsnException(
+				"invalid OID: first byte = " + v);
+		}
+		StringBuilder sb = new StringBuilder();
+		sb.Append(v / 40);
+		sb.Append('.');
+		sb.Append(v % 40);
+		long acc = 0;
+		bool uv = false;
+		for (int i = 1; i < valLen; i ++) {
+			v = objBuf[valOff + i];
+			if ((acc >> 56) != 0) {
+				throw new AsnException(
+					"invalid OID: integer overflow");
+			}
+			acc = (acc << 7) + (long)(v & 0x7F);
+			if ((v & 0x80) == 0) {
+				sb.Append('.');
+				sb.Append(acc);
+				acc = 0;
+				uv = false;
+			} else {
+				uv = true;
+			}
+		}
+		if (uv) {
+			throw new AsnException(
+				"invalid OID: truncated");
+		}
+		return sb.ToString();
+	}
+
+	/*
+	 * Get the object value as a string. The string type is inferred
+	 * from the tag.
+	 */
+	public string GetString()
+	{
+		if (TagClass != UNIVERSAL) {
+			throw new AsnException(String.Format(
+				"cannot infer string type: {0}:{1}",
+				TagClass, TagValue));
+		}
+		return GetString(TagValue);
+	}
+
+	/*
+	 * Get the object value as a string. The string type is provided
+	 * (universal tag value). Supported string types include
+	 * NumericString, PrintableString, IA5String, TeletexString
+	 * (interpreted as ISO-8859-1), UTF8String, BMPString and
+	 * UniversalString; the "time types" (UTCTime and GeneralizedTime)
+	 * are also supported, though, in their case, the internal
+	 * contents are not checked (they are decoded as PrintableString).
+	 */
+	public string GetString(int type)
+	{
+		if (Constructed) {
+			throw new AsnException(
+				"invalid string (constructed)");
+		}
+		switch (type) {
+		case NumericString:
+		case PrintableString:
+		case IA5String:
+		case TeletexString:
+		case UTCTime:
+		case GeneralizedTime:
+			return DecodeMono(objBuf, valOff, valLen, type);
+		case UTF8String:
+			return DecodeUTF8(objBuf, valOff, valLen);
+		case BMPString:
+			return DecodeUTF16(objBuf, valOff, valLen);
+		case UniversalString:
+			return DecodeUTF32(objBuf, valOff, valLen);
+		default:
+			throw new AsnException(
+				"unsupported string type: " + type);
+		}
+	}
+
+	static string DecodeMono(byte[] buf, int off, int len, int type)
+	{
+		char[] tc = new char[len];
+		for (int i = 0; i < len; i ++) {
+			tc[i] = (char)buf[off + i];
+		}
+		VerifyChars(tc, type);
+		return new string(tc);
+	}
+
+	static string DecodeUTF8(byte[] buf, int off, int len)
+	{
+		/*
+		 * Skip BOM.
+		 */
+		if (len >= 3 && buf[off] == 0xEF
+			&& buf[off + 1] == 0xBB && buf[off + 2] == 0xBF)
+		{
+			off += 3;
+			len -= 3;
+		}
+		char[] tc = null;
+		for (int k = 0; k < 2; k ++) {
+			int tcOff = 0;
+			for (int i = 0; i < len; i ++) {
+				int c = buf[off + i];
+				int e;
+				if (c < 0x80) {
+					e = 0;
+				} else if (c < 0xC0) {
+					throw BadByte(c, UTF8String);
+				} else if (c < 0xE0) {
+					c &= 0x1F;
+					e = 1;
+				} else if (c < 0xF0) {
+					c &= 0x0F;
+					e = 2;
+				} else if (c < 0xF8) {
+					c &= 0x07;
+					e = 3;
+				} else {
+					throw BadByte(c, UTF8String);
+				}
+				while (e -- > 0) {
+					if (++ i >= len) {
+						throw new AsnException(
+							"invalid UTF-8 string");
+					}
+					int d = buf[off + i];
+					if (d < 0x80 || d > 0xBF) {
+						throw BadByte(d, UTF8String);
+					}
+					c = (c << 6) + (d & 0x3F);
+				}
+				if (c > 0x10FFFF) {
+					throw BadChar(c, UTF8String);
+				}
+				if (c > 0xFFFF) {
+					c -= 0x10000;
+					int hi = 0xD800 + (c >> 10);
+					int lo = 0xDC00 + (c & 0x3FF);
+					if (tc != null) {
+						tc[tcOff] = (char)hi;
+						tc[tcOff + 1] = (char)lo;
+					}
+					tcOff += 2;
+				} else {
+					if (tc != null) {
+						tc[tcOff] = (char)c;
+					}
+					tcOff ++;
+				}
+			}
+			if (tc == null) {
+				tc = new char[tcOff];
+			}
+		}
+		VerifyChars(tc, UTF8String);
+		return new string(tc);
+	}
+
+	static string DecodeUTF16(byte[] buf, int off, int len)
+	{
+		if ((len & 1) != 0) {
+			throw new AsnException(
+				"invalid UTF-16 string: length = " + len);
+		}
+		len >>= 1;
+		if (len == 0) {
+			return "";
+		}
+		bool be = true;
+		int hi = buf[off];
+		int lo = buf[off + 1];
+		if (hi == 0xFE && lo == 0xFF) {
+			off += 2;
+			len --;
+		} else if (hi == 0xFF && lo == 0xFE) {
+			off += 2;
+			len --;
+			be = false;
+		}
+		char[] tc = new char[len];
+		for (int i = 0; i < len; i ++) {
+			int b0 = buf[off ++];
+			int b1 = buf[off ++];
+			if (be) {
+				tc[i] = (char)((b0 << 8) + b1);
+			} else {
+				tc[i] = (char)((b1 << 8) + b0);
+			}
+		}
+		VerifyChars(tc, BMPString);
+		return new string(tc);
+	}
+
+	static string DecodeUTF32(byte[] buf, int off, int len)
+	{
+		if ((len & 3) != 0) {
+			throw new AsnException(
+				"invalid UTF-32 string: length = " + len);
+		}
+		len >>= 2;
+		if (len == 0) {
+			return "";
+		}
+		bool be = true;
+		if (buf[off] == 0x00
+			&& buf[off + 1] == 0x00
+			&& buf[off + 2] == 0xFE
+			&& buf[off + 3] == 0xFF)
+		{
+			off += 4;
+			len --;
+		} else if (buf[off] == 0xFF
+			&& buf[off + 1] == 0xFE
+			&& buf[off + 2] == 0x00
+			&& buf[off + 3] == 0x00)
+		{
+			off += 4;
+			len --;
+			be = false;
+		}
+
+		char[] tc = null;
+		for (int k = 0; k < 2; k ++) {
+			int tcOff = 0;
+			for (int i = 0; i < len; i ++) {
+				uint b0 = buf[off + 0];
+				uint b1 = buf[off + 1];
+				uint b2 = buf[off + 2];
+				uint b3 = buf[off + 3];
+				uint c;
+				if (be) {
+					c = (b0 << 24) | (b1 << 16)
+						| (b2 << 8) | b3;
+				} else {
+					c = (b3 << 24) | (b2 << 16)
+						| (b1 << 8) | b0;
+				}
+				if (c > 0x10FFFF) {
+					throw BadChar((int)c, UniversalString);
+				}
+				if (c > 0xFFFF) {
+					c -= 0x10000;
+					int hi = 0xD800 + (int)(c >> 10);
+					int lo = 0xDC00 + (int)(c & 0x3FF);
+					if (tc != null) {
+						tc[tcOff] = (char)hi;
+						tc[tcOff + 1] = (char)lo;
+					}
+					tcOff += 2;
+				} else {
+					if (tc != null) {
+						tc[tcOff] = (char)c;
+					}
+					tcOff ++;
+				}
+			}
+			if (tc == null) {
+				tc = new char[tcOff];
+			}
+		}
+		VerifyChars(tc, UniversalString);
+		return new string(tc);
+	}
+
+	static void VerifyChars(char[] tc, int type)
+	{
+		switch (type) {
+		case NumericString:
+			foreach (char c in tc) {
+				if (!IsNum(c)) {
+					throw BadChar(c, type);
+				}
+			}
+			return;
+		case PrintableString:
+		case UTCTime:
+		case GeneralizedTime:
+			foreach (char c in tc) {
+				if (!IsPrintable(c)) {
+					throw BadChar(c, type);
+				}
+			}
+			return;
+		case IA5String:
+			foreach (char c in tc) {
+				if (!IsIA5(c)) {
+					throw BadChar(c, type);
+				}
+			}
+			return;
+		case TeletexString:
+			foreach (char c in tc) {
+				if (!IsLatin1(c)) {
+					throw BadChar(c, type);
+				}
+			}
+			return;
+		}
+
+		/*
+		 * For Unicode string types (UTF-8, BMP...).
+		 */
+		for (int i = 0; i < tc.Length; i ++) {
+			int c = tc[i];
+			if (c >= 0xFDD0 && c <= 0xFDEF) {
+				throw BadChar(c, type);
+			}
+			if (c == 0xFFFE || c == 0xFFFF) {
+				throw BadChar(c, type);
+			}
+			if (c < 0xD800 || c > 0xDFFF) {
+				continue;
+			}
+			if (c > 0xDBFF) {
+				throw BadChar(c, type);
+			}
+			int hi = c & 0x3FF;
+			if (++ i >= tc.Length) {
+				throw BadChar(c, type);
+			}
+			c = tc[i];
+			if (c < 0xDC00 || c > 0xDFFF) {
+				throw BadChar(c, type);
+			}
+			int lo = c & 0x3FF;
+			c = 0x10000 + lo + (hi << 10);
+			if ((c & 0xFFFE) == 0xFFFE) {
+				throw BadChar(c, type);
+			}
+		}
+	}
+
+	static bool IsNum(int c)
+	{
+		return c == ' ' || (c >= '0' && c <= '9');
+	}
+
+	internal static bool IsPrintable(int c)
+	{
+		if (c >= 'A' && c <= 'Z') {
+			return true;
+		}
+		if (c >= 'a' && c <= 'z') {
+			return true;
+		}
+		if (c >= '0' && c <= '9') {
+			return true;
+		}
+		switch (c) {
+		case ' ': case '(': case ')': case '+':
+		case ',': case '-': case '.': case '/':
+		case ':': case '=': case '?': case '\'':
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	static bool IsIA5(int c)
+	{
+		return c < 128;
+	}
+
+	static bool IsLatin1(int c)
+	{
+		return c < 256;
+	}
+
+	static AsnException BadByte(int c, int type)
+	{
+		return new AsnException(String.Format(
+			"unexpected byte 0x{0:X2} in string of type {1}",
+			c, type));
+	}
+
+	static AsnException BadChar(int c, int type)
+	{
+		return new AsnException(String.Format(
+			"unexpected character U+{0:X4} in string of type {1}",
+			c, type));
+	}
+
+	/*
+	 * Decode the value as a date/time. Returned object is in UTC.
+	 * Type of date is inferred from the tag value.
+	 */
+	public DateTime GetTime()
+	{
+		if (TagClass != UNIVERSAL) {
+			throw new AsnException(String.Format(
+				"cannot infer date type: {0}:{1}",
+				TagClass, TagValue));
+		}
+		return GetTime(TagValue);
+	}
+
+	/*
+	 * Decode the value as a date/time. Returned object is in UTC.
+	 * The time string type is provided as parameter (UTCTime or
+	 * GeneralizedTime).
+	 */
+	public DateTime GetTime(int type)
+	{
+		bool isGen = false;
+		switch (type) {
+		case UTCTime:
+			break;
+		case GeneralizedTime:
+			isGen = true;
+			break;
+		default:
+			throw new AsnException(
+				"unsupported date type: " + type);
+		}
+		string s = GetString(type);
+		string orig = s;
+
+		/*
+		 * UTCTime has format:
+		 *    YYMMDDhhmm[ss](Z|(+|-)hhmm)
+		 *
+		 * GeneralizedTime has format:
+		 *    YYYYMMDDhhmmss[.uu*][Z|(+|-)hhmm]
+		 *
+		 * Differences between the two types:
+		 * -- UTCTime encodes year over two digits; GeneralizedTime
+		 * uses four digits. UTCTime years map to 1950..2049 (00 is
+		 * 2000).
+		 * -- Seconds are optional with UTCTime, mandatory with
+		 * GeneralizedTime.
+		 * -- GeneralizedTime can have fractional seconds (optional).
+		 * -- Time zone is optional for GeneralizedTime. However,
+		 * a missing time zone means "local time" which depends on
+		 * the locality, so this is discouraged.
+		 *
+		 * Some other notes:
+		 * -- If there is a fractional second, then it must include
+		 * at least one digit. This implementation processes the
+		 * first three digits, and ignores the rest (if present).
+		 * -- Time zone offset ranges from -23:59 to +23:59.
+		 * -- The calendar computations are delegated to .NET's
+		 * DateTime (and DateTimeOffset) so this implements a
+		 * Gregorian calendar, even for dates before 1589. Year 0
+		 * is not supported.
+		 */
+
+		/*
+		 * Check characters.
+		 */
+		foreach (char c in s) {
+			if (c >= '0' && c <= '9') {
+				continue;
+			}
+			if (c == '.' || c == '+' || c == '-' || c == 'Z') {
+				continue;
+			}
+			throw BadTime(type, orig);
+		}
+
+		bool good = true;
+
+		/*
+		 * Parse the time zone.
+		 */
+		int tzHours = 0;
+		int tzMinutes = 0;
+		bool negZ = false;
+		bool noTZ = false;
+		if (s.EndsWith("Z")) {
+			s = s.Substring(0, s.Length - 1);
+		} else {
+			int j = s.IndexOf('+');
+			if (j < 0) {
+				j = s.IndexOf('-');
+				negZ = true;
+			}
+			if (j < 0) {
+				noTZ = true;
+			} else {
+				string t = s.Substring(j + 1);
+				s = s.Substring(0, j);
+				if (t.Length != 4) {
+					throw BadTime(type, orig);
+				}
+				tzHours = Dec2(t, 0, ref good);
+				tzMinutes = Dec2(t, 2, ref good);
+				if (tzHours < 0 || tzHours > 23
+					|| tzMinutes < 0 || tzMinutes > 59)
+				{
+					throw BadTime(type, orig);
+				}
+			}
+		}
+
+		/*
+		 * Lack of time zone is allowed only for GeneralizedTime.
+		 */
+		if (noTZ && !isGen) {
+			throw BadTime(type, orig);
+		}
+
+		/*
+		 * Parse the date elements.
+		 */
+		if (s.Length < 4) {
+			throw BadTime(type, orig);
+		}
+		int year = Dec2(s, 0, ref good);
+		if (isGen) {
+			year = year * 100 + Dec2(s, 2, ref good);
+			s = s.Substring(4);
+		} else {
+			if (year < 50) {
+				year += 100;
+			}
+			year += 1900;
+			s = s.Substring(2);
+		}
+		int month = Dec2(s, 0, ref good);
+		int day = Dec2(s, 2, ref good);
+		int hour = Dec2(s, 4, ref good);
+		int minute = Dec2(s, 6, ref good);
+		int second = 0;
+		int millisecond = 0;
+		if (isGen) {
+			second = Dec2(s, 8, ref good);
+			if (s.Length >= 12 && s[10] == '.') {
+				s = s.Substring(11);
+				foreach (char c in s) {
+					if (c < '0' || c > '9') {
+						good = false;
+						break;
+					}
+				}
+				s += "0000";
+				millisecond = 10 * Dec2(s, 0, ref good)
+					+ Dec2(s, 2, ref good) / 10;
+			} else if (s.Length != 10) {
+				good = false;
+			}
+		} else {
+			switch (s.Length) {
+			case 8:
+				break;
+			case 10:
+				second = Dec2(s, 8, ref good);
+				break;
+			default:
+				throw BadTime(type, orig);
+			}
+		}
+
+		/*
+		 * Parsing is finished; if any error occurred, then
+		 * the 'good' flag has been cleared.
+		 */
+		if (!good) {
+			throw BadTime(type, orig);
+		}
+
+		/*
+		 * Leap seconds are not supported by .NET, so we claim
+		 * they do not occur.
+		 */
+		if (second == 60) {
+			second = 59;
+		}
+
+		/*
+		 * .NET implementation performs all the checks (including
+		 * checks on month length depending on year, as per the
+		 * proleptic Gregorian calendar).
+		 */
+		try {
+			if (noTZ) {
+				DateTime dt = new DateTime(year, month, day,
+					hour, minute, second, millisecond,
+					DateTimeKind.Local);
+				return dt.ToUniversalTime();
+			}
+			TimeSpan tzOff = new TimeSpan(tzHours, tzMinutes, 0);
+			if (negZ) {
+				tzOff = tzOff.Negate();
+			}
+			DateTimeOffset dto = new DateTimeOffset(
+				year, month, day, hour, minute, second,
+				millisecond, tzOff);
+			return dto.UtcDateTime;
+		} catch (Exception e) {
+			throw BadTime(type, orig, e);
+		}
+	}
+
+	static int Dec2(string s, int off, ref bool good)
+	{
+		if (off < 0 || off >= (s.Length - 1)) {
+			good = false;
+			return -1;
+		}
+		char c1 = s[off];
+		char c2 = s[off + 1];
+		if (c1 < '0' || c1 > '9' || c2 < '0' || c2 > '9') {
+			good = false;
+			return -1;
+		}
+		return 10 * (c1 - '0') + (c2 - '0');
+	}
+
+	static AsnException BadTime(int type, string s)
+	{
+		return BadTime(type, s, null);
+	}
+
+	static AsnException BadTime(int type, string s, Exception e)
+	{
+		string tt = (type == UTCTime) ? "UTCTime" : "GeneralizedTime";
+		string msg = String.Format("invalid {0} string: '{1}'", tt, s);
+		if (e == null) {
+			return new AsnException(msg);
+		} else {
+			return new AsnException(msg, e);
+		}
+	}
+
+	/* =============================================================== */
+
+	/*
+	 * Create a new element for a primitive value. The provided buffer
+	 * is internally copied.
+	 */
+	public static AsnElt MakePrimitive(int tagValue, byte[] val)
+	{
+		return MakePrimitive(UNIVERSAL, tagValue, val, 0, val.Length);
+	}
+
+	/*
+	 * Create a new element for a primitive value. The provided buffer
+	 * is internally copied.
+	 */
+	public static AsnElt MakePrimitive(int tagValue,
+		byte[] val, int off, int len)
+	{
+		return MakePrimitive(UNIVERSAL, tagValue, val, off, len);
+	}
+
+	/*
+	 * Create a new element for a primitive value. The provided buffer
+	 * is internally copied.
+	 */
+	public static AsnElt MakePrimitive(
+		int tagClass, int tagValue, byte[] val)
+	{
+		return MakePrimitive(tagClass, tagValue, val, 0, val.Length);
+	}
+
+	/*
+	 * Create a new element for a primitive value. The provided buffer
+	 * is internally copied.
+	 */
+	public static AsnElt MakePrimitive(int tagClass, int tagValue,
+		byte[] val, int off, int len)
+	{
+		byte[] nval = new byte[len];
+		Array.Copy(val, off, nval, 0, len);
+		return MakePrimitiveInner(tagClass, tagValue, nval, 0, len);
+	}
+
+	/*
+	 * Like MakePrimitive(), but the provided array is NOT copied.
+	 * This is for other factory methods that already allocate a
+	 * new array.
+	 */
+	static AsnElt MakePrimitiveInner(int tagValue, byte[] val)
+	{
+		return MakePrimitiveInner(UNIVERSAL, tagValue,
+			val, 0, val.Length);
+	}
+
+	static AsnElt MakePrimitiveInner(int tagValue,
+		byte[] val, int off, int len)
+	{
+		return MakePrimitiveInner(UNIVERSAL, tagValue, val, off, len);
+	}
+
+	static AsnElt MakePrimitiveInner(int tagClass, int tagValue, byte[] val)
+	{
+		return MakePrimitiveInner(tagClass, tagValue,
+			val, 0, val.Length);
+	}
+
+	static AsnElt MakePrimitiveInner(int tagClass, int tagValue,
+		byte[] val, int off, int len)
+	{
+		AsnElt a = new AsnElt();
+		a.objBuf = new byte[len];
+		Array.Copy(val, off, a.objBuf, 0, len);
+		a.objOff = 0;
+		a.objLen = -1;
+		a.valOff = 0;
+		a.valLen = len;
+		a.hasEncodedHeader = false;
+		if (tagClass < 0 || tagClass > 3) {
+			throw new AsnException(
+				"invalid tag class: " + tagClass);
+		}
+		if (tagValue < 0) {
+			throw new AsnException(
+				"invalid tag value: " + tagValue);
+		}
+		a.TagClass = tagClass;
+		a.TagValue = tagValue;
+		a.Sub = null;
+		return a;
+	}
+
+	/*
+	 * Create a new INTEGER value for the provided integer.
+	 */
+	public static AsnElt MakeInteger(long x)
+	{
+		if (x >= 0) {
+			return MakeInteger((ulong)x);
+		}
+		int k = 1;
+		for (long w = x; w <= -(long)0x80; w >>= 8) {
+			k ++;
+		}
+		byte[] v = new byte[k];
+		for (long w = x; k > 0; w >>= 8) {
+			v[-- k] = (byte)w;
+		}
+		return MakePrimitiveInner(INTEGER, v);
+	}
+
+	/*
+	 * Create a new INTEGER value for the provided integer.
+	 */
+	public static AsnElt MakeInteger(ulong x)
+	{
+		int k = 1;
+		for (ulong w = x; w >= 0x80; w >>= 8) {
+			k ++;
+		}
+		byte[] v = new byte[k];
+		for (ulong w = x; k > 0; w >>= 8) {
+			v[-- k] = (byte)w;
+		}
+		return MakePrimitiveInner(INTEGER, v);
+	}
+
+	/*
+	 * Create a new INTEGER value for the provided integer. The x[]
+	 * array uses _unsigned_ big-endian encoding.
+	 */
+	public static AsnElt MakeInteger(byte[] x)
+	{
+		int xLen = x.Length;
+		int j = 0;
+		while (j < xLen && x[j] == 0x00) {
+			j ++;
+		}
+		if (j == xLen) {
+			return MakePrimitiveInner(INTEGER, new byte[] { 0x00 });
+		}
+		byte[] v;
+		if (x[j] < 0x80) {
+			v = new byte[xLen - j];
+			Array.Copy(x, j, v, 0, v.Length);
+		} else {
+			v = new byte[1 + xLen - j];
+			Array.Copy(x, j, v, 1, v.Length - 1);
+		}
+		return MakePrimitiveInner(INTEGER, v);
+	}
+
+	/*
+	 * Create a new INTEGER value for the provided integer. The x[]
+	 * array uses _signed_ big-endian encoding.
+	 */
+	public static AsnElt MakeIntegerSigned(byte[] x)
+	{
+		int xLen = x.Length;
+		if (xLen == 0) {
+			throw new AsnException(
+				"Invalid signed integer (empty)");
+		}
+		int j = 0;
+		if (x[0] >= 0x80) {
+			while (j < (xLen - 1)
+				&& x[j] == 0xFF
+				&& x[j + 1] >= 0x80)
+			{
+				j ++;
+			}
+		} else {
+			while (j < (xLen - 1)
+				&& x[j] == 0x00
+				&& x[j + 1] < 0x80)
+			{
+				j ++;
+			}
+		}
+		byte[] v = new byte[xLen - j];
+		Array.Copy(x, j, v, 0, v.Length);
+		return MakePrimitiveInner(INTEGER, v);
+	}
+
+	/*
+	 * Create a BIT STRING from the provided value. The number of
+	 * "unused bits" is set to 0.
+	 */
+	public static AsnElt MakeBitString(byte[] buf)
+	{
+		return MakeBitString(buf, 0, buf.Length);
+	}
+
+	public static AsnElt MakeBitString(byte[] buf, int off, int len)
+	{
+		byte[] tmp = new byte[len + 1];
+		Array.Copy(buf, off, tmp, 1, len);
+		return MakePrimitiveInner(BIT_STRING, tmp);
+	}
+
+	/*
+	 * Create a BIT STRING from the provided value. The number of
+	 * "unused bits" is specified.
+	 */
+	public static AsnElt MakeBitString(int unusedBits, byte[] buf)
+	{
+		return MakeBitString(unusedBits, buf, 0, buf.Length);
+	}
+
+	public static AsnElt MakeBitString(int unusedBits,
+		byte[] buf, int off, int len)
+	{
+		if (unusedBits < 0 || unusedBits > 7
+			|| (unusedBits != 0 && len == 0))
+		{
+			throw new AsnException(
+				"Invalid number of unused bits in BIT STRING: "
+				+ unusedBits);
+		}
+		byte[] tmp = new byte[len + 1];
+		tmp[0] = (byte)unusedBits;
+		Array.Copy(buf, off, tmp, 1, len);
+		if (len > 0) {
+			tmp[len - 1] &= (byte)(0xFF << unusedBits);
+		}
+		return MakePrimitiveInner(BIT_STRING, tmp);
+	}
+
+	/*
+	 * Create an OCTET STRING from the provided value.
+	 */
+	public static AsnElt MakeBlob(byte[] buf)
+	{
+		return MakeBlob(buf, 0, buf.Length);
+	}
+
+	public static AsnElt MakeBlob(byte[] buf, int off, int len)
+	{
+		return MakePrimitive(OCTET_STRING, buf, off, len);
+	}
+
+	/*
+	 * Create a new constructed elements, by providing the relevant
+	 * sub-elements.
+	 */
+	public static AsnElt Make(int tagValue, params AsnElt[] subs)
+	{
+		return Make(UNIVERSAL, tagValue, subs);
+	}
+
+	/*
+	 * Create a new constructed elements, by providing the relevant
+	 * sub-elements.
+	 */
+	public static AsnElt Make(int tagClass, int tagValue,
+		params AsnElt[] subs)
+	{
+		AsnElt a = new AsnElt();
+		a.objBuf = null;
+		a.objOff = 0;
+		a.objLen = -1;
+		a.valOff = 0;
+		a.valLen = -1;
+		a.hasEncodedHeader = false;
+		if (tagClass < 0 || tagClass > 3) {
+			throw new AsnException(
+				"invalid tag class: " + tagClass);
+		}
+		if (tagValue < 0) {
+			throw new AsnException(
+				"invalid tag value: " + tagValue);
+		}
+		a.TagClass = tagClass;
+		a.TagValue = tagValue;
+		if (subs == null) {
+			a.Sub = new AsnElt[0];
+		} else {
+			a.Sub = new AsnElt[subs.Length];
+			Array.Copy(subs, 0, a.Sub, 0, subs.Length);
+		}
+		return a;
+	}
+
+	/*
+	 * Create a SET OF: sub-elements are automatically sorted by
+	 * lexicographic order of their DER encodings. Identical elements
+	 * are merged.
+	 */
+	public static AsnElt MakeSetOf(params AsnElt[] subs)
+	{
+		AsnElt a = new AsnElt();
+		a.objBuf = null;
+		a.objOff = 0;
+		a.objLen = -1;
+		a.valOff = 0;
+		a.valLen = -1;
+		a.hasEncodedHeader = false;
+		a.TagClass = UNIVERSAL;
+		a.TagValue = SET;
+		if (subs == null) {
+			a.Sub = new AsnElt[0];
+		} else {
+			SortedDictionary<byte[], AsnElt> d =
+				new SortedDictionary<byte[], AsnElt>(
+					COMPARER_LEXICOGRAPHIC);
+			foreach (AsnElt ax in subs) {
+				d[ax.Encode()] = ax;
+			}
+			AsnElt[] tmp = new AsnElt[d.Count];
+			int j = 0;
+			foreach (AsnElt ax in d.Values) {
+				tmp[j ++] = ax;
+			}
+			a.Sub = tmp;
+		}
+		return a;
+	}
+
+	static IComparer<byte[]> COMPARER_LEXICOGRAPHIC =
+		new ComparerLexicographic();
+
+	class ComparerLexicographic : IComparer<byte[]> {
+
+		public int Compare(byte[] x, byte[] y)
+		{
+			int xLen = x.Length;
+			int yLen = y.Length;
+			int cLen = Math.Min(xLen, yLen);
+			for (int i = 0; i < cLen; i ++) {
+				if (x[i] != y[i]) {
+					return (int)x[i] - (int)y[i];
+				}
+			}
+			return xLen - yLen;
+		}
+	}
+
+	/*
+	 * Wrap an element into an explicit tag.
+	 */
+	public static AsnElt MakeExplicit(int tagClass, int tagValue, AsnElt x)
+	{
+		return Make(tagClass, tagValue, x);
+	}
+
+	/*
+	 * Wrap an element into an explicit CONTEXT tag.
+	 */
+	public static AsnElt MakeExplicit(int tagValue, AsnElt x)
+	{
+		return Make(CONTEXT, tagValue, x);
+	}
+
+	/*
+	 * Apply an implicit tag to a value. The source AsnElt object
+	 * is unmodified; a new object is returned.
+	 */
+	public static AsnElt MakeImplicit(int tagClass, int tagValue, AsnElt x)
+	{
+		if (x.Constructed) {
+			return Make(tagClass, tagValue, x.Sub);
+		}
+		AsnElt a = new AsnElt();
+		a.objBuf = x.GetValue(out a.valOff, out a.valLen);
+		a.objOff = 0;
+		a.objLen = -1;
+		a.hasEncodedHeader = false;
+		a.TagClass = tagClass;
+		a.TagValue = tagValue;
+		a.Sub = null;
+		return a;
+	}
+
+	public static AsnElt NULL_V = AsnElt.MakePrimitive(
+		NULL, new byte[0]);
+
+	public static AsnElt BOOL_TRUE = AsnElt.MakePrimitive(
+		BOOLEAN, new byte[] { 0xFF });
+	public static AsnElt BOOL_FALSE = AsnElt.MakePrimitive(
+		BOOLEAN, new byte[] { 0x00 });
+
+	/*
+	 * Create an OBJECT IDENTIFIER from its string representation.
+	 * This function tolerates extra leading zeros.
+	 */
+	public static AsnElt MakeOID(string str)
+	{
+		List<long> r = new List<long>();
+		int n = str.Length;
+		long x = -1;
+		for (int i = 0; i < n; i ++) {
+			int c = str[i];
+			if (c == '.') {
+				if (x < 0) {
+					throw new AsnException(
+						"invalid OID (empty element)");
+				}
+				r.Add(x);
+				x = -1;
+				continue;
+			}
+			if (c < '0' || c > '9') {
+				throw new AsnException(String.Format(
+					"invalid character U+{0:X4} in OID",
+					c));
+			}
+			if (x < 0) {
+				x = 0;
+			} else if (x > ((Int64.MaxValue - 9) / 10)) {
+				throw new AsnException("OID element overflow");
+			}
+			x = x * (long)10 + (long)(c - '0');
+		}
+		if (x < 0) {
+			throw new AsnException(
+				"invalid OID (empty element)");
+		}
+		r.Add(x);
+		if (r.Count < 2) {
+			throw new AsnException(
+				"invalid OID (not enough elements)");
+		}
+		if (r[0] > 2 || r[1] > 40) {
+			throw new AsnException(
+				"invalid OID (first elements out of range)");
+		}
+
+		MemoryStream ms = new MemoryStream();
+		ms.WriteByte((byte)(40 * (int)r[0] + (int)r[1]));
+		for (int i = 2; i < r.Count; i ++) {
+			long v = r[i];
+			if (v < 0x80) {
+				ms.WriteByte((byte)v);
+				continue;
+			}
+			int k = -7;
+			for (long w = v; w != 0; w >>= 7, k += 7);
+			ms.WriteByte((byte)(0x80 + (int)(v >> k)));
+			for (k -= 7; k >= 0; k -= 7) {
+				int z = (int)(v >> k) & 0x7F;
+				if (k > 0) {
+					z |= 0x80;
+				}
+				ms.WriteByte((byte)z);
+			}
+		}
+		byte[] buf = ms.ToArray();
+		return MakePrimitiveInner(OBJECT_IDENTIFIER,
+			buf, 0, buf.Length);
+	}
+
+	/*
+	 * Create a string of the provided type and contents. The string
+	 * type is a universal tag value for one of the string or time
+	 * types.
+	 */
+	public static AsnElt MakeString(int type, string str)
+	{
+		VerifyChars(str.ToCharArray(), type);
+		byte[] buf;
+		switch (type) {
+		case NumericString:
+		case PrintableString:
+		case UTCTime:
+		case GeneralizedTime:
+		case IA5String:
+		case TeletexString:
+			buf = EncodeMono(str);
+			break;
+		case UTF8String:
+			buf = EncodeUTF8(str);
+			break;
+		case BMPString:
+			buf = EncodeUTF16(str);
+			break;
+		case UniversalString:
+			buf = EncodeUTF32(str);
+			break;
+		default:
+			throw new AsnException(
+				"unsupported string type: " + type);
+		}
+		return MakePrimitiveInner(type, buf);
+	}
+
+	static byte[] EncodeMono(string str)
+	{
+		byte[] r = new byte[str.Length];
+		int k = 0;
+		foreach (char c in str) {
+			r[k ++] = (byte)c;
+		}
+		return r;
+	}
+
+	/*
+	 * Get the code point at offset 'off' in the string. Either one
+	 * or two 'char' slots are used; 'off' is updated accordingly.
+	 */
+	static int CodePoint(string str, ref int off)
+	{
+		int c = str[off ++];
+		if (c >= 0xD800 && c < 0xDC00 && off < str.Length) {
+			int d = str[off];
+			if (d >= 0xDC00 && d < 0xE000) {
+				c = ((c & 0x3FF) << 10)
+					+ (d & 0x3FF) + 0x10000;
+				off ++;
+			}
+		}
+		return c;
+	}
+
+	static byte[] EncodeUTF8(string str)
+	{
+		int k = 0;
+		int n = str.Length;
+		MemoryStream ms = new MemoryStream();
+		while (k < n) {
+			int cp = CodePoint(str, ref k);
+			if (cp < 0x80) {
+				ms.WriteByte((byte)cp);
+			} else if (cp < 0x800) {
+				ms.WriteByte((byte)(0xC0 + (cp >> 6)));
+				ms.WriteByte((byte)(0x80 + (cp & 63)));
+			} else if (cp < 0x10000) {
+				ms.WriteByte((byte)(0xE0 + (cp >> 12)));
+				ms.WriteByte((byte)(0x80 + ((cp >> 6) & 63)));
+				ms.WriteByte((byte)(0x80 + (cp & 63)));
+			} else {
+				ms.WriteByte((byte)(0xF0 + (cp >> 18)));
+				ms.WriteByte((byte)(0x80 + ((cp >> 12) & 63)));
+				ms.WriteByte((byte)(0x80 + ((cp >> 6) & 63)));
+				ms.WriteByte((byte)(0x80 + (cp & 63)));
+			}
+		}
+		return ms.ToArray();
+	}
+
+	static byte[] EncodeUTF16(string str)
+	{
+		byte[] buf = new byte[str.Length << 1];
+		int k = 0;
+		foreach (char c in str) {
+			buf[k ++] = (byte)(c >> 8);
+			buf[k ++] = (byte)c;
+		}
+		return buf;
+	}
+
+	static byte[] EncodeUTF32(string str)
+	{
+		int k = 0;
+		int n = str.Length;
+		MemoryStream ms = new MemoryStream();
+		while (k < n) {
+			int cp = CodePoint(str, ref k);
+			ms.WriteByte((byte)(cp >> 24));
+			ms.WriteByte((byte)(cp >> 16));
+			ms.WriteByte((byte)(cp >> 8));
+			ms.WriteByte((byte)cp);
+		}
+		return ms.ToArray();
+	}
+
+	/*
+	 * Create a time value of the specified type (UTCTime or
+	 * GeneralizedTime).
+	 */
+	public static AsnElt MakeTime(int type, DateTime dt)
+	{
+		dt = dt.ToUniversalTime();
+		string str;
+		switch (type) {
+		case UTCTime:
+			int year = dt.Year;
+			if (year < 1950 || year >= 2050) {
+				throw new AsnException(String.Format(
+					"cannot encode year {0} as UTCTime",
+					year));
+			}
+			year = year % 100;
+			str = String.Format(
+				"{0:d2}{1:d2}{2:d2}{3:d2}{4:d2}{5:d2}Z",
+				year, dt.Month, dt.Day,
+				dt.Hour, dt.Minute, dt.Second);
+			break;
+		case GeneralizedTime:
+			str = String.Format(
+				"{0:d4}{1:d2}{2:d2}{3:d2}{4:d2}{5:d2}",
+				dt.Year, dt.Month, dt.Day,
+				dt.Hour, dt.Minute, dt.Second);
+			int millis = dt.Millisecond;
+			if (millis != 0) {
+				if (millis % 100 == 0) {
+					str = String.Format("{0}.{1:d1}",
+						str, millis / 100);
+				} else if (millis % 10 == 0) {
+					str = String.Format("{0}.{1:d2}",
+						str, millis / 10);
+				} else {
+					str = String.Format("{0}.{1:d3}",
+						str, millis);
+				}
+			}
+			str = str + "Z";
+			break;
+		default:
+			throw new AsnException(
+				"unsupported time type: " + type);
+		}
+		return MakeString(type, str);
+	}
+
+	/*
+	 * Create a time value of the specified type (UTCTime or
+	 * GeneralizedTime).
+	 */
+	public static AsnElt MakeTime(int type, DateTimeOffset dto)
+	{
+		return MakeTime(type, dto.UtcDateTime);
+	}
+
+	/*
+	 * Create a time value with an automatic type selection
+	 * (UTCTime if year is in the 1950..2049 range, GeneralizedTime
+	 * otherwise).
+	 */
+	public static AsnElt MakeTimeAuto(DateTime dt)
+	{
+		dt = dt.ToUniversalTime();
+		return MakeTime((dt.Year >= 1950 && dt.Year <= 2049)
+			? UTCTime : GeneralizedTime, dt);
+	}
+
+	/*
+	 * Create a time value with an automatic type selection
+	 * (UTCTime if year is in the 1950..2049 range, GeneralizedTime
+	 * otherwise).
+	 */
+	public static AsnElt MakeTimeAuto(DateTimeOffset dto)
+	{
+		return MakeTimeAuto(dto.UtcDateTime);
+	}
+}
+
+}
diff --git a/Asn1/AsnException.cs b/Asn1/AsnException.cs
new file mode 100644
index 0000000..a2e6555
--- /dev/null
+++ b/Asn1/AsnException.cs
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.IO;
+
+namespace Asn1 {
+
+/*
+ * An AsnException is thrown whenever decoding of an object fails. It is
+ * made a subclass of IOException under the assumption that decoding
+ * failures mostly occur when processing incoming data from external
+ * sources.
+ */
+
+public class AsnException : IOException {
+
+	public AsnException(string message)
+		: base(message)
+	{
+	}
+
+	public AsnException(string message, Exception nested)
+		: base(message, nested)
+	{
+	}
+}
+
+}
diff --git a/Asn1/AsnIO.cs b/Asn1/AsnIO.cs
new file mode 100644
index 0000000..fb8cd3f
--- /dev/null
+++ b/Asn1/AsnIO.cs
@@ -0,0 +1,402 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace Asn1 {
+
+/*
+ * Helper functions for located BER/DER-encoded object, and in particular
+ * handle PEM format.
+ */
+
+public static class AsnIO {
+
+	public static byte[] FindDER(byte[] buf)
+	{
+		return FindBER(buf, true);
+	}
+
+	public static byte[] FindBER(byte[] buf)
+	{
+		return FindBER(buf, false);
+	}
+
+	/*
+	 * Find a BER/DER object in the provided buffer. If the data is
+	 * not already in the right format, conversion to string then
+	 * Base64 decoding is attempted; in the latter case, PEM headers
+	 * are detected and skipped. In any case, the returned buffer
+	 * must begin with a well-formed tag and length, corresponding to
+	 * the object length.
+	 *
+	 * If 'strictDER' is true, then the function furthermore insists
+	 * on the object to use a defined DER length.
+	 *
+	 * The returned buffer may be the source buffer itself, or a newly
+	 * allocated buffer.
+	 *
+	 * On error, null is returned.
+	 */
+	public static byte[] FindBER(byte[] buf, bool strictDER)
+	{
+		string pemType = null;
+		return FindBER(buf, strictDER, out pemType);
+	}
+
+	/*
+	 * Find a BER/DER object in the provided buffer. If the data is
+	 * not already in the right format, conversion to string then
+	 * Base64 decoding is attempted; in the latter case, PEM headers
+	 * are detected and skipped. In any case, the returned buffer
+	 * must begin with a well-formed tag and length, corresponding to
+	 * the object length.
+	 *
+	 * If 'strictDER' is true, then the function furthermore insists
+	 * on the object to use a defined DER length.
+	 *
+	 * If the source was detected to use PEM, then the object type
+	 * indicated by the PEM header is written in 'pemType'; otherwise,
+	 * that variable is set to null.
+	 *
+	 * The returned buffer may be the source buffer itself, or a newly
+	 * allocated buffer.
+	 *
+	 * On error, null is returned.
+	 */
+	public static byte[] FindBER(byte[] buf,
+		bool strictDER, out string pemType)
+	{
+		pemType = null;
+
+		/*
+		 * If it is already (from the outside) a BER object,
+		 * return it.
+		 */
+		if (LooksLikeBER(buf, strictDER)) {
+			return buf;
+		}
+
+		string str = BinToString(buf);
+		if (str == null) {
+			return null;
+		}
+
+		/*
+		 * Try to detect a PEM header and footer; if we find both
+		 * then we remove both, keeping only the characters that
+		 * occur in between.
+		 */
+		int p = str.IndexOf("-----BEGIN ");
+		int q = str.IndexOf("-----END ");
+		if (p >= 0 && q >= 0) {
+			p += 11;
+			int r = str.IndexOf((char)10, p) + 1;
+			int px = str.IndexOf('-', p);
+			if (px > 0 && px < r && r > 0 && r <= q) {
+				pemType = string.Copy(str.Substring(p, px - p));
+				str = str.Substring(r, q - r);
+			}
+		}
+
+		/*
+		 * Convert from Base64.
+		 */
+		try {
+			buf = Convert.FromBase64String(str);
+			if (LooksLikeBER(buf, strictDER)) {
+				return buf;
+			}
+		} catch {
+			// ignored: not Base64
+		}
+
+		/*
+		 * Decoding failed.
+		 */
+		return null;
+	}
+
+	/*
+	 * Decode multiple PEM objects from a file. Each object is
+	 * returned with its name.
+	 */
+	public static PEMObject[] DecodePEM(byte[] buf)
+	{
+		string str = BinToString(buf);
+		if (str == null) {
+			return new PEMObject[0];
+		}
+
+		List<PEMObject> fpo = new List<PEMObject>();
+		TextReader tr = new StringReader(str);
+		StringBuilder sb = new StringBuilder();
+		string currentType = null;
+		for (;;) {
+			string line = tr.ReadLine();
+			if (line == null) {
+				break;
+			}
+			if (currentType == null) {
+				if (line.StartsWith("-----BEGIN ")) {
+					line = line.Substring(11);
+					int n = line.IndexOf("-----");
+					if (n >= 0) {
+						line = line.Substring(0, n);
+					}
+					currentType = string.Copy(line);
+					sb = new StringBuilder();
+				}
+				continue;
+			}
+			if (line.StartsWith("-----END ")) {
+				string s = sb.ToString();
+				try {
+					byte[] data =
+						Convert.FromBase64String(s);
+					fpo.Add(new PEMObject(
+						currentType, data));
+				} catch {
+					/*
+					 * Base64 decoding failed... we skip
+					 * the object.
+					 */
+				}
+				currentType = null;
+				sb.Clear();
+				continue;
+			}
+			sb.Append(line);
+			sb.Append("\n");
+		}
+		return fpo.ToArray();
+	}
+
+	/* =============================================================== */
+
+	/*
+	 * Decode a tag; returned value is true on success, false otherwise.
+	 * On success, 'off' is updated to point to the first byte after
+	 * the tag.
+	 */
+	static bool DecodeTag(byte[] buf, int lim, ref int off)
+	{
+		int p = off;
+		if (p >= lim) {
+			return false;
+		}
+		int v = buf[p ++];
+		if ((v & 0x1F) == 0x1F) {
+			do {
+				if (p >= lim) {
+					return false;
+				}
+				v = buf[p ++];
+			} while ((v & 0x80) != 0);
+		}
+		off = p;
+		return true;
+	}
+
+	/*
+	 * Decode a BER length. Returned value is:
+	 *   -2   no decodable length
+	 *   -1   indefinite length
+	 *    0+  definite length
+	 * If a definite or indefinite length could be decoded, then 'off'
+	 * is updated to point to the first byte after the length.
+	 */
+	static int DecodeLength(byte[] buf, int lim, ref int off)
+	{
+		int p = off;
+		if (p >= lim) {
+			return -2;
+		}
+		int v = buf[p ++];
+		if (v < 0x80) {
+			off = p;
+			return v;
+		} else if (v == 0x80) {
+			off = p;
+			return -1;
+		}
+		v &= 0x7F;
+		if ((lim - p) < v) {
+			return -2;
+		}
+		int acc = 0;
+		while (v -- > 0) {
+			if (acc > 0x7FFFFF) {
+				return -2;
+			}
+			acc = (acc << 8) + buf[p ++];
+		}
+		off = p;
+		return acc;
+	}
+
+	/*
+	 * Get the length, in bytes, of the object in the provided
+	 * buffer. The object begins at offset 'off' but does not extend
+	 * farther than offset 'lim'. If no such BER object can be
+	 * decoded, then -1 is returned. The returned length includes
+	 * that of the tag and length fields.
+	 */
+	static int BERLength(byte[] buf, int lim, int off)
+	{
+		int orig = off;
+		if (!DecodeTag(buf, lim, ref off)) {
+			return -1;
+		}
+		int len = DecodeLength(buf, lim, ref off);
+		if (len >= 0) {
+			if (len > (lim - off)) {
+				return -1;
+			}
+			return off + len - orig;
+		} else if (len < -1) {
+			return -1;
+		}
+
+		/*
+		 * Indefinite length: we must do some recursive exploration.
+		 * End of structure is marked by a "null tag": object has
+		 * total length 2 and its tag byte is 0.
+		 */
+		for (;;) {
+			int slen = BERLength(buf, lim, off);
+			if (slen < 0) {
+				return -1;
+			}
+			off += slen;
+			if (slen == 2 && buf[off] == 0) {
+				return off - orig;
+			}
+		}
+	}
+
+	static bool LooksLikeBER(byte[] buf, bool strictDER)
+	{
+		return LooksLikeBER(buf, 0, buf.Length, strictDER);
+	}
+
+	static bool LooksLikeBER(byte[] buf, int off, int len, bool strictDER)
+	{
+		int lim = off + len;
+		int objLen = BERLength(buf, lim, off);
+		if (objLen != len) {
+			return false;
+		}
+		if (strictDER) {
+			DecodeTag(buf, lim, ref off);
+			return DecodeLength(buf, lim, ref off) >= 0;
+		} else {
+			return true;
+		}
+	}
+
+	static string ConvertMono(byte[] buf, int off)
+	{
+		int len = buf.Length - off;
+		char[] tc = new char[len];
+		for (int i = 0; i < len; i ++) {
+			int v = buf[off + i];
+			if (v < 1 || v > 126) {
+				v = '?';
+			}
+			tc[i] = (char)v;
+		}
+		return new string(tc);
+	}
+
+	static string ConvertBi(byte[] buf, int off, bool be)
+	{
+		int len = buf.Length - off;
+		if ((len & 1) != 0) {
+			return null;
+		}
+		len >>= 1;
+		char[] tc = new char[len];
+		for (int i = 0; i < len; i ++) {
+			int b0 = buf[off + (i << 1) + 0];
+			int b1 = buf[off + (i << 1) + 1];
+			int v = be ? ((b0 << 8) + b1) : (b0 + (b1 << 8));
+			if (v < 1 || v > 126) {
+				v = '?';
+			}
+			tc[i] = (char)v;
+		}
+		return new string(tc);
+	}
+
+	/*
+	 * Convert a blob to a string. This supports UTF-16 (with and
+	 * without a BOM), UTF-8 (with and without a BOM), and
+	 * ASCII-compatible encodings. Non-ASCII characters get
+	 * replaced with '?'. This function is meant to be used
+	 * with heuristic PEM decoders.
+	 *
+	 * If conversion is not possible, then null is returned.
+	 */
+	static string BinToString(byte[] buf)
+	{
+		if (buf.Length < 3) {
+			return null;
+		}
+		string str = null;
+		if ((buf.Length & 1) == 0) {
+			if (buf[0] == 0xFE && buf[1] == 0xFF) {
+				// Starts with big-endian UTF-16 BOM
+				str = ConvertBi(buf, 2, true);
+			} else if (buf[0] == 0xFF && buf[1] == 0xFE) {
+				// Starts with little-endian UTF-16 BOM
+				str = ConvertBi(buf, 2, false);
+			} else if (buf[0] == 0) {
+				// First byte is 0 -> big-endian UTF-16
+				str = ConvertBi(buf, 0, true);
+			} else if (buf[1] == 0) {
+				// Second byte is 0 -> little-endian UTF-16
+				str = ConvertBi(buf, 0, false);
+			}
+		}
+		if (str == null) {
+			if (buf[0] == 0xEF
+				&& buf[1] == 0xBB
+				&& buf[2] == 0xBF)
+			{
+				// Starts with UTF-8 BOM
+				str = ConvertMono(buf, 3);
+			} else {
+				// Assumed ASCII-compatible mono-byte encoding
+				str = ConvertMono(buf, 0);
+			}
+		}
+		return str;
+	}
+}
+
+}
diff --git a/Asn1/AsnOID.cs b/Asn1/AsnOID.cs
new file mode 100644
index 0000000..e794356
--- /dev/null
+++ b/Asn1/AsnOID.cs
@@ -0,0 +1,335 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace Asn1 {
+
+/*
+ * This class contains some helper methods to convert known OID to
+ * symbolic names and back.
+ */
+
+public class AsnOID {
+
+	static Dictionary<string, string> OIDToName =
+		new Dictionary<string, string>();
+	static Dictionary<string, string> NameToOID =
+		new Dictionary<string, string>();
+
+	static AsnOID()
+	{
+		/*
+		 * From RFC 5280, PKIX1Explicit88 module.
+		 */
+		Reg("1.3.6.1.5.5.7", "id-pkix");
+		Reg("1.3.6.1.5.5.7.1", "id-pe");
+		Reg("1.3.6.1.5.5.7.2", "id-qt");
+		Reg("1.3.6.1.5.5.7.3", "id-kp");
+		Reg("1.3.6.1.5.5.7.48", "id-ad");
+		Reg("1.3.6.1.5.5.7.2.1", "id-qt-cps");
+		Reg("1.3.6.1.5.5.7.2.2", "id-qt-unotice");
+		Reg("1.3.6.1.5.5.7.48.1", "id-ad-ocsp");
+		Reg("1.3.6.1.5.5.7.48.2", "id-ad-caIssuers");
+		Reg("1.3.6.1.5.5.7.48.3", "id-ad-timeStamping");
+		Reg("1.3.6.1.5.5.7.48.5", "id-ad-caRepository");
+
+		Reg("2.5.4", "id-at");
+		Reg("2.5.4.41", "id-at-name");
+		Reg("2.5.4.4", "id-at-surname");
+		Reg("2.5.4.42", "id-at-givenName");
+		Reg("2.5.4.43", "id-at-initials");
+		Reg("2.5.4.44", "id-at-generationQualifier");
+		Reg("2.5.4.3", "id-at-commonName");
+		Reg("2.5.4.7", "id-at-localityName");
+		Reg("2.5.4.8", "id-at-stateOrProvinceName");
+		Reg("2.5.4.10", "id-at-organizationName");
+		Reg("2.5.4.11", "id-at-organizationalUnitName");
+		Reg("2.5.4.12", "id-at-title");
+		Reg("2.5.4.46", "id-at-dnQualifier");
+		Reg("2.5.4.6", "id-at-countryName");
+		Reg("2.5.4.5", "id-at-serialNumber");
+		Reg("2.5.4.65", "id-at-pseudonym");
+		Reg("0.9.2342.19200300.100.1.25", "id-domainComponent");
+
+		Reg("1.2.840.113549.1.9", "pkcs-9");
+		Reg("1.2.840.113549.1.9.1", "id-emailAddress");
+
+		/*
+		 * From RFC 5280, PKIX1Implicit88 module.
+		 */
+		Reg("2.5.29", "id-ce");
+		Reg("2.5.29.35", "id-ce-authorityKeyIdentifier");
+		Reg("2.5.29.14", "id-ce-subjectKeyIdentifier");
+		Reg("2.5.29.15", "id-ce-keyUsage");
+		Reg("2.5.29.16", "id-ce-privateKeyUsagePeriod");
+		Reg("2.5.29.32", "id-ce-certificatePolicies");
+		Reg("2.5.29.33", "id-ce-policyMappings");
+		Reg("2.5.29.17", "id-ce-subjectAltName");
+		Reg("2.5.29.18", "id-ce-issuerAltName");
+		Reg("2.5.29.9", "id-ce-subjectDirectoryAttributes");
+		Reg("2.5.29.19", "id-ce-basicConstraints");
+		Reg("2.5.29.30", "id-ce-nameConstraints");
+		Reg("2.5.29.36", "id-ce-policyConstraints");
+		Reg("2.5.29.31", "id-ce-cRLDistributionPoints");
+		Reg("2.5.29.37", "id-ce-extKeyUsage");
+
+		Reg("2.5.29.37.0", "anyExtendedKeyUsage");
+		Reg("1.3.6.1.5.5.7.3.1", "id-kp-serverAuth");
+		Reg("1.3.6.1.5.5.7.3.2", "id-kp-clientAuth");
+		Reg("1.3.6.1.5.5.7.3.3", "id-kp-codeSigning");
+		Reg("1.3.6.1.5.5.7.3.4", "id-kp-emailProtection");
+		Reg("1.3.6.1.5.5.7.3.8", "id-kp-timeStamping");
+		Reg("1.3.6.1.5.5.7.3.9", "id-kp-OCSPSigning");
+
+		Reg("2.5.29.54", "id-ce-inhibitAnyPolicy");
+		Reg("2.5.29.46", "id-ce-freshestCRL");
+		Reg("1.3.6.1.5.5.7.1.1", "id-pe-authorityInfoAccess");
+		Reg("1.3.6.1.5.5.7.1.11", "id-pe-subjectInfoAccess");
+		Reg("2.5.29.20", "id-ce-cRLNumber");
+		Reg("2.5.29.28", "id-ce-issuingDistributionPoint");
+		Reg("2.5.29.27", "id-ce-deltaCRLIndicator");
+		Reg("2.5.29.21", "id-ce-cRLReasons");
+		Reg("2.5.29.29", "id-ce-certificateIssuer");
+		Reg("2.5.29.23", "id-ce-holdInstructionCode");
+		Reg("2.2.840.10040.2", "WRONG-holdInstruction");
+		Reg("2.2.840.10040.2.1", "WRONG-id-holdinstruction-none");
+		Reg("2.2.840.10040.2.2", "WRONG-id-holdinstruction-callissuer");
+		Reg("2.2.840.10040.2.3", "WRONG-id-holdinstruction-reject");
+		Reg("2.5.29.24", "id-ce-invalidityDate");
+
+		/*
+		 * These are the "right" OID. RFC 5280 mistakenly defines
+		 * the first OID element as "2".
+		 */
+		Reg("1.2.840.10040.2", "holdInstruction");
+		Reg("1.2.840.10040.2.1", "id-holdinstruction-none");
+		Reg("1.2.840.10040.2.2", "id-holdinstruction-callissuer");
+		Reg("1.2.840.10040.2.3", "id-holdinstruction-reject");
+
+		/*
+		 * From PKCS#1.
+		 */
+		Reg("1.2.840.113549.1.1", "pkcs-1");
+		Reg("1.2.840.113549.1.1.1", "rsaEncryption");
+		Reg("1.2.840.113549.1.1.7", "id-RSAES-OAEP");
+		Reg("1.2.840.113549.1.1.9", "id-pSpecified");
+		Reg("1.2.840.113549.1.1.10", "id-RSASSA-PSS");
+		Reg("1.2.840.113549.1.1.2", "md2WithRSAEncryption");
+		Reg("1.2.840.113549.1.1.4", "md5WithRSAEncryption");
+		Reg("1.2.840.113549.1.1.5", "sha1WithRSAEncryption");
+		Reg("1.2.840.113549.1.1.11", "sha256WithRSAEncryption");
+		Reg("1.2.840.113549.1.1.12", "sha384WithRSAEncryption");
+		Reg("1.2.840.113549.1.1.13", "sha512WithRSAEncryption");
+		Reg("1.3.14.3.2.26", "id-sha1");
+		Reg("1.2.840.113549.2.2", "id-md2");
+		Reg("1.2.840.113549.2.5", "id-md5");
+		Reg("1.2.840.113549.1.1.8", "id-mgf1");
+
+		/*
+		 * From NIST: http://csrc.nist.gov/groups/ST/crypto_apps_infra/csor/algorithms.html
+		 */
+		Reg("2.16.840.1.101.3", "csor");
+		Reg("2.16.840.1.101.3.4", "nistAlgorithms");
+		Reg("2.16.840.1.101.3.4.0", "csorModules");
+		Reg("2.16.840.1.101.3.4.0.1", "aesModule1");
+
+		Reg("2.16.840.1.101.3.4.1", "aes");
+		Reg("2.16.840.1.101.3.4.1.1", "id-aes128-ECB");
+		Reg("2.16.840.1.101.3.4.1.2", "id-aes128-CBC");
+		Reg("2.16.840.1.101.3.4.1.3", "id-aes128-OFB");
+		Reg("2.16.840.1.101.3.4.1.4", "id-aes128-CFB");
+		Reg("2.16.840.1.101.3.4.1.5", "id-aes128-wrap");
+		Reg("2.16.840.1.101.3.4.1.6", "id-aes128-GCM");
+		Reg("2.16.840.1.101.3.4.1.7", "id-aes128-CCM");
+		Reg("2.16.840.1.101.3.4.1.8", "id-aes128-wrap-pad");
+		Reg("2.16.840.1.101.3.4.1.21", "id-aes192-ECB");
+		Reg("2.16.840.1.101.3.4.1.22", "id-aes192-CBC");
+		Reg("2.16.840.1.101.3.4.1.23", "id-aes192-OFB");
+		Reg("2.16.840.1.101.3.4.1.24", "id-aes192-CFB");
+		Reg("2.16.840.1.101.3.4.1.25", "id-aes192-wrap");
+		Reg("2.16.840.1.101.3.4.1.26", "id-aes192-GCM");
+		Reg("2.16.840.1.101.3.4.1.27", "id-aes192-CCM");
+		Reg("2.16.840.1.101.3.4.1.28", "id-aes192-wrap-pad");
+		Reg("2.16.840.1.101.3.4.1.41", "id-aes256-ECB");
+		Reg("2.16.840.1.101.3.4.1.42", "id-aes256-CBC");
+		Reg("2.16.840.1.101.3.4.1.43", "id-aes256-OFB");
+		Reg("2.16.840.1.101.3.4.1.44", "id-aes256-CFB");
+		Reg("2.16.840.1.101.3.4.1.45", "id-aes256-wrap");
+		Reg("2.16.840.1.101.3.4.1.46", "id-aes256-GCM");
+		Reg("2.16.840.1.101.3.4.1.47", "id-aes256-CCM");
+		Reg("2.16.840.1.101.3.4.1.48", "id-aes256-wrap-pad");
+
+		Reg("2.16.840.1.101.3.4.2", "hashAlgs");
+		Reg("2.16.840.1.101.3.4.2.1", "id-sha256");
+		Reg("2.16.840.1.101.3.4.2.2", "id-sha384");
+		Reg("2.16.840.1.101.3.4.2.3", "id-sha512");
+		Reg("2.16.840.1.101.3.4.2.4", "id-sha224");
+		Reg("2.16.840.1.101.3.4.2.5", "id-sha512-224");
+		Reg("2.16.840.1.101.3.4.2.6", "id-sha512-256");
+
+		Reg("2.16.840.1.101.3.4.3", "sigAlgs");
+		Reg("2.16.840.1.101.3.4.3.1", "id-dsa-with-sha224");
+		Reg("2.16.840.1.101.3.4.3.2", "id-dsa-with-sha256");
+
+		Reg("1.2.840.113549", "rsadsi");
+		Reg("1.2.840.113549.2", "digestAlgorithm");
+		Reg("1.2.840.113549.2.7", "id-hmacWithSHA1");
+		Reg("1.2.840.113549.2.8", "id-hmacWithSHA224");
+		Reg("1.2.840.113549.2.9", "id-hmacWithSHA256");
+		Reg("1.2.840.113549.2.10", "id-hmacWithSHA384");
+		Reg("1.2.840.113549.2.11", "id-hmacWithSHA512");
+
+		/*
+		 * From X9.57: http://oid-info.com/get/1.2.840.10040.4
+		 */
+		Reg("1.2.840.10040.4", "x9algorithm");
+		Reg("1.2.840.10040.4", "x9cm");
+		Reg("1.2.840.10040.4.1", "dsa");
+		Reg("1.2.840.10040.4.3", "dsa-with-sha1");
+
+		/*
+		 * From SEC: http://oid-info.com/get/1.3.14.3.2
+		 */
+		Reg("1.3.14.3.2.2", "md4WithRSA");
+		Reg("1.3.14.3.2.3", "md5WithRSA");
+		Reg("1.3.14.3.2.4", "md4WithRSAEncryption");
+		Reg("1.3.14.3.2.12", "dsaSEC");
+		Reg("1.3.14.3.2.13", "dsaWithSHASEC");
+		Reg("1.3.14.3.2.27", "dsaWithSHA1SEC");
+
+		/*
+		 * From Microsoft: http://oid-info.com/get/1.3.6.1.4.1.311.20.2
+		 */
+		Reg("1.3.6.1.4.1.311.20.2", "ms-certType");
+		Reg("1.3.6.1.4.1.311.20.2.2", "ms-smartcardLogon");
+		Reg("1.3.6.1.4.1.311.20.2.3", "ms-UserPrincipalName");
+		Reg("1.3.6.1.4.1.311.20.2.3", "ms-UPN");
+	}
+
+	static void Reg(string oid, string name)
+	{
+		if (!OIDToName.ContainsKey(oid)) {
+			OIDToName.Add(oid, name);
+		}
+		string nn = Normalize(name);
+		if (NameToOID.ContainsKey(nn)) {
+			throw new Exception("OID name collision: " + nn);
+		}
+		NameToOID.Add(nn, oid);
+
+		/*
+		 * Many names start with 'id-??-' and we want to support
+		 * the short names (without that prefix) as aliases. But
+		 * we must take care of some collisions on short names.
+		 */
+		if (name.StartsWith("id-")
+			&& name.Length >= 7 && name[5] == '-')
+		{
+			if (name.StartsWith("id-ad-")) {
+				Reg(oid, name.Substring(6) + "-IA");
+			} else if (name.StartsWith("id-kp-")) {
+				Reg(oid, name.Substring(6) + "-EKU");
+			} else {
+				Reg(oid, name.Substring(6));
+			}
+		}
+	}
+
+	static string Normalize(string name)
+	{
+		StringBuilder sb = new StringBuilder();
+		foreach (char c in name) {
+			int d = (int)c;
+			if (d <= 32 || d == '-') {
+				continue;
+			}
+			if (d >= 'A' && d <= 'Z') {
+				d += 'a' - 'A';
+			}
+			sb.Append((char)c);
+		}
+		return sb.ToString();
+	}
+
+	/*
+	 * Convert an OID in numeric form to its symbolic name, if known.
+	 * Otherwise, the provided string is returned as is.
+	 */
+	public static string ToName(string oid)
+	{
+		return OIDToName.ContainsKey(oid) ? OIDToName[oid] : oid;
+	}
+
+	/*
+	 * Convert a symbolic OID name to its numeric representation. If
+	 * the source string is already numeric OID, then it is returned
+	 * as is. Otherwise, the symbolic name is looked up; this lookup
+	 * ignores case as well as spaces and dash characters. If the name
+	 * is not recognized, an AsnException is thrown.
+	 */
+	public static string ToOID(string name)
+	{
+		if (IsNumericOID(name)) {
+			return name;
+		}
+		string nn = Normalize(name);
+		if (!NameToOID.ContainsKey(nn)) {
+			throw new AsnException(
+				"unrecognized OID name: " + name);
+		}
+		return NameToOID[nn];
+	}
+
+	/*
+	 * Test whether a given string "looks like" an OID in numeric
+	 * representation. Criteria applied by this function:
+	 *  - only decimal ASCII digits and dots
+	 *  - does not start or end with a dot
+	 *  - at least one dot
+	 *  - no two consecutive dots
+	 */
+	public static bool IsNumericOID(string oid)
+	{
+		foreach (char c in oid) {
+			if (!(c >= '0' && c <= '9') && c != '.') {
+				return false;
+			}
+		}
+		if (oid.StartsWith(".") || oid.EndsWith(".")) {
+			return false;
+		}
+		if (oid.IndexOf("..") >= 0) {
+			return false;
+		}
+		if (oid.IndexOf('.') < 0) {
+			return false;
+		}
+		return true;
+	}
+}
+
+}
diff --git a/Asn1/PEMObject.cs b/Asn1/PEMObject.cs
new file mode 100644
index 0000000..9be6c1a
--- /dev/null
+++ b/Asn1/PEMObject.cs
@@ -0,0 +1,47 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Asn1 {
+
+/*
+ * A PEMObject is a structure that corresponds to a binary object that
+ * was found PEM-encoded in some source stream. The object contents and
+ * PEM type are provided.
+ */
+
+public struct PEMObject {
+
+	public string type;
+	public byte[] data;
+
+	public PEMObject(string type, byte[] data)
+	{
+		this.type = type;
+		this.data = data;
+	}
+}
+
+}
diff --git a/Crypto/AES.cs b/Crypto/AES.cs
new file mode 100644
index 0000000..1ffd454
--- /dev/null
+++ b/Crypto/AES.cs
@@ -0,0 +1,433 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * Classic AES implementation, with tables (8->32 lookup tables, four
+ * tables for encryption, four other tables for decryption). This is
+ * relatively efficient, but not constant-time.
+ */
+
+public sealed class AES : BlockCipherCore {
+
+	uint[] skey;
+	uint[] iskey;
+	int rounds;
+
+	/*
+	 * Initialize a new instance.
+	 */
+	public AES()
+	{
+		skey = new uint[4 * 15];
+		iskey = new uint[skey.Length];
+	}
+
+	/* see IBlockCipher */
+	public override IBlockCipher Dup()
+	{
+		AES a = new AES();
+		Array.Copy(skey, 0, a.skey, 0, skey.Length);
+		Array.Copy(iskey, 0, a.iskey, 0, iskey.Length);
+		a.rounds = rounds;
+		return a;
+	}
+
+	/*
+	 * Get the block size in bytes (always 16).
+	 */
+	public override int BlockSize {
+		get {
+			return 16;
+		}
+	}
+
+	/*
+	 * Set the key (16, 24 or 32 bytes).
+	 */
+	public override void SetKey(byte[] key, int off, int len)
+	{
+		switch (len) {
+		case 16:
+			rounds = 10;
+			break;
+		case 24:
+			rounds = 12;
+			break;
+		case 32:
+			rounds = 14;
+			break;
+		default:
+			throw new ArgumentException(
+				"bad AES key length: " + len);
+		}
+		int nk = len >> 2;
+		int nkf = (rounds + 1) << 2;
+		for (int i = 0; i < nk; i ++) {
+			skey[i] = Dec32be(key, off + (i << 2));
+		}
+		for (int i = nk, j = 0, k = 0; i < nkf; i ++) {
+			uint tmp = skey[i - 1];
+			if (j == 0) {
+				tmp = (tmp << 8) | (tmp >> 24);
+				tmp = SubWord(tmp) ^ Rcon[k];
+			} else if (nk > 6 && j == 4) {
+				tmp = SubWord(tmp);
+			}
+			skey[i] = skey[i - nk] ^ tmp;
+			if (++ j == nk) {
+				j = 0;
+				k ++;
+			}
+		}
+
+		/*
+		 * Subkeys for decryption (with InvMixColumns() already
+		 * applied for the inner rounds).
+		 */
+		Array.Copy(skey, 0, iskey, 0, 4);
+		for (int i = 4; i < (rounds << 2); i ++) {
+			uint p = skey[i];
+			uint p0 = p >> 24;
+			uint p1 = (p >> 16) & 0xFF;
+			uint p2 = (p >> 8) & 0xFF;
+			uint p3 = p & 0xFF;
+			uint q0 = mule(p0) ^ mulb(p1) ^ muld(p2) ^ mul9(p3);
+			uint q1 = mul9(p0) ^ mule(p1) ^ mulb(p2) ^ muld(p3);
+			uint q2 = muld(p0) ^ mul9(p1) ^ mule(p2) ^ mulb(p3);
+			uint q3 = mulb(p0) ^ muld(p1) ^ mul9(p2) ^ mule(p3);
+			iskey[i] = (q0 << 24) | (q1 << 16) | (q2 << 8) | q3;
+		}
+		Array.Copy(skey, rounds << 2, iskey, rounds << 2, 4);
+	}
+
+	/* see IBlockCipher */
+	public override void BlockEncrypt(byte[] buf, int off)
+	{
+		uint s0 = Dec32be(buf, off);
+		uint s1 = Dec32be(buf, off + 4);
+		uint s2 = Dec32be(buf, off + 8);
+		uint s3 = Dec32be(buf, off + 12);
+		s0 ^= skey[0];
+		s1 ^= skey[1];
+		s2 ^= skey[2];
+		s3 ^= skey[3];
+		for (int i = 1; i < rounds; i ++) {
+			uint v0 = Ssm0[s0 >> 24]
+				^ Ssm1[(s1 >> 16) & 0xFF]
+				^ Ssm2[(s2 >> 8) & 0xFF]
+				^ Ssm3[s3 & 0xFF];
+			uint v1 = Ssm0[s1 >> 24]
+				^ Ssm1[(s2 >> 16) & 0xFF]
+				^ Ssm2[(s3 >> 8) & 0xFF]
+				^ Ssm3[s0 & 0xFF];
+			uint v2 = Ssm0[s2 >> 24]
+				^ Ssm1[(s3 >> 16) & 0xFF]
+				^ Ssm2[(s0 >> 8) & 0xFF]
+				^ Ssm3[s1 & 0xFF];
+			uint v3 = Ssm0[s3 >> 24]
+				^ Ssm1[(s0 >> 16) & 0xFF]
+				^ Ssm2[(s1 >> 8) & 0xFF]
+				^ Ssm3[s2 & 0xFF];
+			s0 = v0;
+			s1 = v1;
+			s2 = v2;
+			s3 = v3;
+			s0 ^= skey[i << 2];
+			s1 ^= skey[(i << 2) + 1];
+			s2 ^= skey[(i << 2) + 2];
+			s3 ^= skey[(i << 2) + 3];
+		}
+		uint t0 = (S[s0 >> 24] << 24)
+			| (S[(s1 >> 16) & 0xFF] << 16)
+			| (S[(s2 >> 8) & 0xFF] << 8)
+			| S[s3 & 0xFF];
+		uint t1 = (S[s1 >> 24] << 24)
+			| (S[(s2 >> 16) & 0xFF] << 16)
+			| (S[(s3 >> 8) & 0xFF] << 8)
+			| S[s0 & 0xFF];
+		uint t2 = (S[s2 >> 24] << 24)
+			| (S[(s3 >> 16) & 0xFF] << 16)
+			| (S[(s0 >> 8) & 0xFF] << 8)
+			| S[s1 & 0xFF];
+		uint t3 = (S[s3 >> 24] << 24)
+			| (S[(s0 >> 16) & 0xFF] << 16)
+			| (S[(s1 >> 8) & 0xFF] << 8)
+			| S[s2 & 0xFF];
+		s0 = t0 ^ skey[rounds << 2];
+		s1 = t1 ^ skey[(rounds << 2) + 1];
+		s2 = t2 ^ skey[(rounds << 2) + 2];
+		s3 = t3 ^ skey[(rounds << 2) + 3];
+		Enc32be(s0, buf, off);
+		Enc32be(s1, buf, off + 4);
+		Enc32be(s2, buf, off + 8);
+		Enc32be(s3, buf, off + 12);
+	}
+
+	/* see IBlockCipher */
+	public override void BlockDecrypt(byte[] buf, int off)
+	{
+		uint s0 = Dec32be(buf, off);
+		uint s1 = Dec32be(buf, off + 4);
+		uint s2 = Dec32be(buf, off + 8);
+		uint s3 = Dec32be(buf, off + 12);
+		s0 ^= iskey[rounds << 2];
+		s1 ^= iskey[(rounds << 2) + 1];
+		s2 ^= iskey[(rounds << 2) + 2];
+		s3 ^= iskey[(rounds << 2) + 3];
+		for (int i = rounds - 1; i > 0; i --) {
+			uint v0 = iSsm0[s0 >> 24]
+				^ iSsm1[(s3 >> 16) & 0xFF]
+				^ iSsm2[(s2 >> 8) & 0xFF]
+				^ iSsm3[s1 & 0xFF];
+			uint v1 = iSsm0[s1 >> 24]
+				^ iSsm1[(s0 >> 16) & 0xFF]
+				^ iSsm2[(s3 >> 8) & 0xFF]
+				^ iSsm3[s2 & 0xFF];
+			uint v2 = iSsm0[s2 >> 24]
+				^ iSsm1[(s1 >> 16) & 0xFF]
+				^ iSsm2[(s0 >> 8) & 0xFF]
+				^ iSsm3[s3 & 0xFF];
+			uint v3 = iSsm0[s3 >> 24]
+				^ iSsm1[(s2 >> 16) & 0xFF]
+				^ iSsm2[(s1 >> 8) & 0xFF]
+				^ iSsm3[s0 & 0xFF];
+			s0 = v0;
+			s1 = v1;
+			s2 = v2;
+			s3 = v3;
+			s0 ^= iskey[i << 2];
+			s1 ^= iskey[(i << 2) + 1];
+			s2 ^= iskey[(i << 2) + 2];
+			s3 ^= iskey[(i << 2) + 3];
+		}
+		uint t0 = (iS[s0 >> 24] << 24)
+			| (iS[(s3 >> 16) & 0xFF] << 16)
+			| (iS[(s2 >> 8) & 0xFF] << 8)
+			| iS[s1 & 0xFF];
+		uint t1 = (iS[s1 >> 24] << 24)
+			| (iS[(s0 >> 16) & 0xFF] << 16)
+			| (iS[(s3 >> 8) & 0xFF] << 8)
+			| iS[s2 & 0xFF];
+		uint t2 = (iS[s2 >> 24] << 24)
+			| (iS[(s1 >> 16) & 0xFF] << 16)
+			| (iS[(s0 >> 8) & 0xFF] << 8)
+			| iS[s3 & 0xFF];
+		uint t3 = (iS[s3 >> 24] << 24)
+			| (iS[(s2 >> 16) & 0xFF] << 16)
+			| (iS[(s1 >> 8) & 0xFF] << 8)
+			| iS[s0 & 0xFF];
+		s0 = t0 ^ iskey[0];
+		s1 = t1 ^ iskey[1];
+		s2 = t2 ^ iskey[2];
+		s3 = t3 ^ iskey[3];
+		Enc32be(s0, buf, off);
+		Enc32be(s1, buf, off + 4);
+		Enc32be(s2, buf, off + 8);
+		Enc32be(s3, buf, off + 12);
+	}
+
+	static uint Dec32be(byte[] buf, int off)
+	{
+		return ((uint)buf[off] << 24)
+			| ((uint)buf[off + 1] << 16)
+			| ((uint)buf[off + 2] << 8)
+			| (uint)buf[off + 3];
+	}
+
+	static void Enc32be(uint x, byte[] buf, int off)
+	{
+		buf[off] = (byte)(x >> 24);
+		buf[off + 1] = (byte)(x >> 16);
+		buf[off + 2] = (byte)(x >> 8);
+		buf[off + 3] = (byte)x;
+	}
+
+	static uint mul2(uint x)
+	{
+		x <<= 1;
+		return x ^ ((uint)(-(int)(x >> 8)) & 0x11B);
+	}
+
+	static uint mul3(uint x)
+	{
+		return x ^ mul2(x);
+	}
+
+	static uint mul9(uint x)
+	{
+		return x ^ mul2(mul2(mul2(x)));
+	}
+
+	static uint mulb(uint x)
+	{
+		uint x2 = mul2(x);
+		return x ^ x2 ^ mul2(mul2(x2));
+	}
+
+	static uint muld(uint x)
+	{
+		uint x4 = mul2(mul2(x));
+		return x ^ x4 ^ mul2(x4);
+	}
+
+	static uint mule(uint x)
+	{
+		uint x2 = mul2(x);
+		uint x4 = mul2(x2);
+		return x2 ^ x4 ^ mul2(x4);
+	}
+
+	static uint aff(uint x)
+	{
+		x |= x << 8;
+		x ^= (x >> 4) ^ (x >> 5) ^ (x >> 6) ^ (x >> 7) ^ 0x63;
+		return x & 0xFF;
+	}
+
+	static uint[] Rcon;
+	static uint[] S;
+	static uint[] Ssm0, Ssm1, Ssm2, Ssm3;
+	static uint[] iS;
+	static uint[] iSsm0, iSsm1, iSsm2, iSsm3;
+
+	static uint SubWord(uint x)
+	{
+		return (S[x >> 24] << 24)
+			| (S[(x >> 16) & 0xFF] << 16)
+			| (S[(x >> 8) & 0xFF] << 8)
+			| S[x & 0xFF];
+	}
+
+	static AES()
+	{
+		/*
+		 * The Rcon[] constants are used in the key schedule.
+		 */
+		Rcon = new uint[10];
+		uint x = 1;
+		for (int i = 0; i < Rcon.Length; i ++) {
+			Rcon[i] = x << 24;
+			x = mul2(x);
+		}
+
+		/*
+		 * Generate the map x -> 3^x in GF(2^8). "3" happens to
+		 * be a generator for GF(2^8)*, so we get all 255 non-zero
+		 * elements.
+		 */
+		uint[] pow3 = new uint[255];
+		x = 1;
+		for (int i = 0; i < 255; i ++) {
+			pow3[i] = x;
+			x ^= mul2(x);
+		}
+
+		/*
+		 * Compute the log3 map 3^x -> x that maps any non-zero
+		 * element in GF(2^8) to its logarithm in base 3 (in the
+		 * 0..254 range).
+		 */
+		int[] log3 = new int[256];
+		for (int i = 0; i < 255; i ++) {
+			log3[pow3[i]] = i;
+		}
+
+		/*
+		 * Compute the S-box.
+		 */
+		S = new uint[256];
+		S[0] = aff(0);
+		S[1] = aff(1);
+		for (uint y = 2; y < 0x100; y ++) {
+			S[y] = aff(pow3[255 - log3[y]]);
+		}
+
+		/*
+		 * Compute the inverse S-box (for decryption).
+		 */
+		iS = new uint[256];
+		for (uint y = 0; y < 0x100; y ++) {
+			iS[S[y]] = y;
+		}
+
+		/*
+		 * The Ssm*[] arrays combine SubBytes() and MixColumns():
+		 * SsmX[v] is the effect of byte of value v when appearing
+		 * on row X.
+		 *
+		 * The iSsm*[] arrays similarly combine InvSubBytes() and
+		 * InvMixColumns(), for decryption.
+		 */
+		Ssm0 = new uint[256];
+		Ssm1 = new uint[256];
+		Ssm2 = new uint[256];
+		Ssm3 = new uint[256];
+		iSsm0 = new uint[256];
+		iSsm1 = new uint[256];
+		iSsm2 = new uint[256];
+		iSsm3 = new uint[256];
+		for (uint p = 0; p < 0x100; p ++) {
+			uint q = S[p];
+			Ssm0[p] = (mul2(q) << 24)
+				| (q << 16)
+				| (q << 8)
+				| mul3(q);
+			Ssm1[p] = (mul3(q) << 24)
+				| (mul2(q) << 16)
+				| (q << 8)
+				| q;
+			Ssm2[p] = (q << 24)
+				| (mul3(q) << 16)
+				| (mul2(q) << 8)
+				| q;
+			Ssm3[p] = (q << 24)
+				| (q << 16)
+				| (mul3(q) << 8)
+				| mul2(q);
+			q = iS[p];
+			iSsm0[p] = (mule(q) << 24)
+				| (mul9(q) << 16)
+				| (muld(q) << 8)
+				| mulb(q);
+			iSsm1[p] = (mulb(q) << 24)
+				| (mule(q) << 16)
+				| (mul9(q) << 8)
+				| muld(q);
+			iSsm2[p] = (muld(q) << 24)
+				| (mulb(q) << 16)
+				| (mule(q) << 8)
+				| mul9(q);
+			iSsm3[p] = (mul9(q) << 24)
+				| (muld(q) << 16)
+				| (mulb(q) << 8)
+				| mule(q);
+		}
+	}
+}
+
+}
diff --git a/Crypto/BigInt.cs b/Crypto/BigInt.cs
new file mode 100644
index 0000000..893aa7c
--- /dev/null
+++ b/Crypto/BigInt.cs
@@ -0,0 +1,580 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * Helper methods for handling big integers.
+ */
+
+static class BigInt {
+
+	/* 
+	 * Normalize a big integer to its minimal size encoding
+	 * (unsigned big-endian). This function always returns
+	 * a new, fresh array.
+	 */
+	internal static byte[] NormalizeBE(byte[] val)
+	{
+		return NormalizeBE(val, false);
+	}
+
+	/*
+	 * Normalize a big integer to its minimal size encoding
+	 * (big-endian). If 'signed' is true, then room for a sign
+	 * bit (of value 0) is kept. Note that even if 'signed' is
+	 * true, the source array is assumed positive (i.e. unsigned).
+	 * This function always returns a new, fresh array.
+	 */
+	internal static byte[] NormalizeBE(byte[] val, bool signed)
+	{
+		int n = val.Length;
+		int i = 0;
+		while (i < n && val[i] == 0) {
+			i ++;
+		}
+		if (signed && (i == n || val[i] >= 0x80)) {
+			i --;
+		}
+		byte[] nval = new byte[n - i];
+		if (i < 0) {
+			Array.Copy(val, 0, nval, 1, n);
+		} else {
+			Array.Copy(val, i, nval, 0, n - i);
+		}
+		return nval;
+	}
+
+	/*
+	 * Compute the exact bit length of an integer (unsigned big-endian
+	 * encoding).
+	 */
+	internal static int BitLength(byte[] val)
+	{
+		return BitLength(val, 0, val.Length);
+	}
+
+	/*
+	 * Compute the exact bit length of an integer (unsigned big-endian
+	 * encoding).
+	 */
+	internal static int BitLength(byte[] val, int off, int len)
+	{
+		int tlen = 0;
+		uint hb = 0;
+		uint nf = ~(uint)0;
+		for (int i = 0; i < len; i ++) {
+			int b = (int)(val[off + i] & nf);
+			uint bnz = nf & (uint)((b | -b) >> 31);
+			tlen |= (int)bnz & (len - i);
+			hb |= bnz & (uint)b;
+			nf &= ~bnz;
+		}
+		return (tlen << 3) - 8 + BitLength(hb);
+	}
+
+	/*
+	 * Compute the exact bit length of an integer (in a 32-bit word).
+	 */
+	internal static int BitLength(uint w)
+	{
+		int bitLen = 0;
+		for (int f = 16; f > 0; f >>= 1) {
+			uint nw = w >> f;
+			int x = (int)nw;
+			uint ctl = (uint)((x | -x) >> 31);
+			w = (w & ~ctl) | (nw & ctl);
+			bitLen += f & (int)ctl;
+		}
+		return bitLen + (int)w;
+	}
+
+	/*
+	 * Compute a simple hashcode on an integer. The returned value
+	 * depends on the numerical value (assuming that the array is
+	 * in unsigned big-endian representation) but not on the presence
+	 * of leading zeros. The memory access pattern of this method
+	 * depends only on the length of the array x.
+	 */
+	public static uint HashInt(byte[] x)
+	{
+		/*
+		 * We simply compute x modulo 4294967291 (0xFFFFFFFB),
+		 * which is a prime number. The value is injected byte
+		 * by byte, and we keep the running state on two words
+		 * (16 bits per word, but we allow a few extra bits of
+		 * carry).
+		 *
+		 * For all iterations, "hi" contains at most 16 bits,
+		 * and "lo" is less than 16*2^16 (i.e. it fits on 20 bits).
+		 */
+		uint hi = 0, lo = 0;
+		for (int i = 0; i < x.Length; i ++) {
+			hi = (hi << 8) + (lo >> 8);
+			lo = (lo & 0xFF) << 8;
+			lo += (uint)5 * (hi >> 16) + (uint)x[i];
+			hi &= 0xFFFF;
+		}
+
+		/*
+		 * Final reduction. We first propagate the extra bits
+		 * from the low word, which may induce one extra bit
+		 * on the high word, which we propagate back.
+		 */
+		hi += (lo >> 16);
+		lo &= 0xFFFF;
+		lo += (uint)5 * (hi >> 16);
+		hi &= 0xFFFF;
+
+		/*
+		 * If we have extra bits at that point, then this means
+		 * that adding a 4-bits-or-less value to "hi" implied
+		 * a carry, so now "hi" is small and the addition below
+		 * won't imply a carry.
+		 */
+		hi += (lo >> 16);
+		lo &= 0xFFFF;
+
+		/*
+		 * At that point, value is on 32 bits. We want to do a
+		 * final reduction for 0xFFFFFFFB..0xFFFFFFFF. Value is
+		 * in this range if and only if 'hi' is 0xFFFF and 'lo'
+		 * is at least 0xFFFB.
+		 */
+		int z = (int)(((hi + 1) >> 16) | ((lo + 5) >> 16));
+		return (hi << 16) + lo + ((uint)5 & (uint)((z | -z) >> 31));
+	}
+
+	/*
+	 * Add two integers together. The two operands a[] and b[]
+	 * use big-endian encoding. The returned product array is newly
+	 * allocated and normalized. The operands are not modified. The
+	 * operands need not be normalized and may be the same array.
+	 */
+	public static byte[] Add(byte[] a, byte[] b)
+	{
+		int aLen = a.Length;
+		int bLen = b.Length;
+		int xLen = Math.Max(aLen, bLen) + 1;
+		byte[] x = new byte[xLen];
+		int cc = 0;
+		for (int i = 0; i < xLen; i ++) {
+			int wa = (i < aLen) ? (int)a[aLen - 1 - i] : 0;
+			int wb = (i < bLen) ? (int)b[bLen - 1 - i] : 0;
+			int wx = wa + wb + cc;
+			x[xLen - 1 - i] = (byte)wx;
+			cc = wx >> 8;
+		}
+		return NormalizeBE(x);
+	}
+
+	/*
+	 * Subtract integer b[] from integer a[]. Both operands use
+	 * big-endian encoding. If b[] turns out to be greater than a[],
+	 * then this method returns null.
+	 */
+	public static byte[] Sub(byte[] a, byte[] b)
+	{
+		int aLen = a.Length;
+		int bLen = b.Length;
+		int xLen = Math.Max(aLen, bLen);
+		byte[] x = new byte[aLen];
+		int cc = 0;
+		for (int i = 0; i < xLen; i ++) {
+			int wa = (i < aLen) ? (int)a[aLen - 1 - i] : 0;
+			int wb = (i < bLen) ? (int)b[bLen - 1 - i] : 0;
+			int wx = wa - wb - cc;
+			x[xLen - 1 - i] = (byte)wx;
+			cc = (wx >> 8) & 1;
+		}
+		if (cc != 0) {
+			return null;
+		}
+		return NormalizeBE(x);
+	}
+
+	/*
+	 * Multiply two integers together. The two operands a[] and b[]
+	 * use big-endian encoding. The returned product array is newly
+	 * allocated and normalized. The operands are not modified. The
+	 * operands need not be normalized and may be the same array.
+	 *
+	 * The two source operands MUST NOT have length larger than
+	 * 32767 bytes.
+	 */
+	public static byte[] Mul(byte[] a, byte[] b)
+	{
+		a = NormalizeBE(a);
+		b = NormalizeBE(b);
+		if (a.Length > 32767 || b.Length > 32767) {
+			throw new CryptoException(
+				"Operands too large for multiplication");
+		}
+		int aLen = a.Length;
+		int bLen = b.Length;
+		int xLen = aLen + bLen;
+		uint[] x = new uint[xLen];
+		for (int i = 0; i < aLen; i ++) {
+			uint u = (uint)a[aLen - 1 - i];
+			for (int j = 0; j < bLen; j ++) {
+				x[i + j] += u * (uint)b[bLen - 1 - j];
+			}
+		}
+		byte[] y = new byte[xLen];
+		uint cc = 0;
+		for (int i = 0; i < xLen; i ++) {
+			uint w = x[i] + cc;
+			y[xLen - 1 - i] = (byte)w;
+			cc = w >> 8;
+		}
+		if (cc != 0) {
+			throw new CryptoException(
+				"Multiplication: internal error");
+		}
+		return NormalizeBE(y);
+	}
+
+	/*
+	 * Compare two integers (unsigned, big-endian). Returned value
+	 * is -1, 0 or 1, depending on whether a[] is lower than, equal
+	 * to, or greater then b[]. a[] and b[] may have distinct sizes.
+	 *
+	 * Memory access pattern depends on the most significant index
+	 * (i.e. lowest index, since we use big-endian convention) for
+	 * which the two values differ.
+	 */
+	public static int Compare(byte[] a, byte[] b)
+	{
+		int na = a.Length;
+		int nb = b.Length;
+		for (int i = Math.Max(a.Length, b.Length); i > 0; i --) {
+			byte xa = (i > na) ? (byte)0x00 : a[na - i];
+			byte xb = (i > nb) ? (byte)0x00 : b[nb - i];
+			if (xa != xb) {
+				return xa < xb ? -1 : 1;
+			}
+		}
+		return 0;
+	}
+
+	/*
+	 * Compare two integers (unsigned, big-endian). Returned value
+	 * is -1, 0 or 1, depending on whether a[] is lower than, equal
+	 * to, or greater then b[]. a[] and b[] may have distinct sizes.
+	 *
+	 * This method's memory access pattern is independent of the
+	 * contents of the a[] and b[] arrays.
+	 */
+	public static int CompareCT(byte[] a, byte[] b)
+	{
+		int na = a.Length;
+		int nb = b.Length;
+		uint lt = 0;
+		uint gt = 0;
+		for (int i = Math.Max(a.Length, b.Length); i > 0; i --) {
+			int xa = (i > na) ? 0 : a[na - i];
+			int xb = (i > nb) ? 0 : b[nb - i];
+			lt |= (uint)((xa - xb) >> 31) & ~(lt | gt);
+			gt |= (uint)((xb - xa) >> 31) & ~(lt | gt);
+		}
+		return (int)lt | -(int)gt;
+	}
+
+	/*
+	 * Check whether an integer (unsigned, big-endian) is zero.
+	 */
+	public static bool IsZero(byte[] x)
+	{
+		return IsZeroCT(x) != 0;
+	}
+
+	/*
+	 * Check whether an integer (unsigned, big-endian) is zero.
+	 * Memory access pattern depends only on the length of x[].
+	 * Returned value is 0xFFFFFFFF is the value is zero, 0x00000000
+	 * otherwise.
+	 */
+	public static uint IsZeroCT(byte[] x)
+	{
+		int z = 0;
+		for (int i = 0; i < x.Length; i ++) {
+			z |= x[i];
+		}
+		return ~(uint)((z | -z) >> 31);
+	}
+
+	/*
+	 * Check whether an integer (unsigned, big-endian) is one.
+	 */
+	public static bool IsOne(byte[] x)
+	{
+		return IsOneCT(x) != 0;
+	}
+
+	/*
+	 * Check whether an integer (unsigned, big-endian) is one.
+	 * Memory access pattern depends only on the length of x[].
+	 * Returned value is 0xFFFFFFFF is the value is one, 0x00000000
+	 * otherwise.
+	 */
+	public static uint IsOneCT(byte[] x)
+	{
+		int n = x.Length;
+		if (n == 0) {
+			return 0x00000000;
+		}
+		int z = 0;
+		for (int i = 0; i < n - 1; i ++) {
+			z |= x[i];
+		}
+		z |= x[n - 1] - 1;
+		return ~(uint)((z | -z) >> 31);
+	}
+
+	/*
+	 * Check whether the provided integer is odd (the source integer
+	 * is in unsigned big-endian notation).
+	 */
+	public static bool IsOdd(byte[] x)
+	{
+		return x.Length > 0 && (x[x.Length - 1] & 0x01) != 0;
+	}
+
+	/*
+	 * Compute a modular exponentiation (x^e mod n). Conditions:
+	 * -- x[], e[] and n[] use big-endian encoding.
+	 * -- x[] must be numerically smaller than n[].
+	 * -- n[] must be odd.
+	 * Result is returned as a newly allocated array of bytes of
+	 * the same length as n[].
+	 */
+	public static byte[] ModPow(byte[] x, byte[] e, byte[] n)
+	{
+		ModInt mx = new ModInt(n);
+		mx.Decode(x);
+		mx.Pow(e);
+		return mx.Encode();
+	}
+
+	/*
+	 * Create a new random integer, chosen uniformly among integers
+	 * modulo the provided max[].
+	 */
+	public static byte[] RandInt(byte[] max)
+	{
+		return RandInt(max, false);
+	}
+
+	/*
+	 * Create a new random integer, chosen uniformly among non-zero
+	 * integers modulo the provided max[].
+	 */
+	public static byte[] RandIntNZ(byte[] max)
+	{
+		return RandInt(max, true);
+	}
+
+	static byte[] RandInt(byte[] max, bool notZero)
+	{
+		int mlen = BitLength(max);
+		if (mlen == 0 || (notZero && mlen == 1)) {
+			throw new CryptoException(
+				"Null maximum for random generation");
+		}
+		byte[] x = new byte[(mlen + 7) >> 3];
+		byte hm = (byte)(0xFF >> ((8 - mlen) & 7));
+		for (;;) {
+			RNG.GetBytes(x);
+			x[0] &= hm;
+			if (notZero && IsZero(x)) {
+				continue;
+			}
+			if (CompareCT(x, max) >= 0) {
+				continue;
+			}
+			return x;
+		}
+	}
+
+	/*
+	 * Create a new random prime with a specific length (in bits). The
+	 * returned prime will have its two top bits set, _and_ its two
+	 * least significant bits set as well. The size parameter must be
+	 * greater than or equal to 9 (that is, the unsigned encoding of
+	 * the prime will need at least two bytes).
+	 */
+	public static byte[] RandPrime(int size)
+	{
+		if (size < 9) {
+			throw new CryptoException(
+				"Invalid size for prime generation");
+		}
+		int len = (size + 7) >> 3;
+		byte[] buf = new byte[len];
+		int hm1 = 0xFFFF >> ((len << 3) - size);
+		int hm2 = 0xC000 >> ((len << 3) - size);
+		for (;;) {
+			RNG.GetBytes(buf);
+			buf[len - 1] |= (byte)0x03;
+			int x = (buf[0] << 8) | buf[1];
+			x &= hm1;
+			x |= hm2;
+			buf[0] = (byte)(x >> 8);
+			buf[1] = (byte)x;
+			if (IsPrime(buf)) {
+				return buf;
+			}
+		}
+	}
+
+	/*
+	 * A bit-field for primes in the 0..255 range.
+	 */
+	static uint[] SMALL_PRIMES_BF = {
+		0xA08A28AC, 0x28208A20, 0x02088288, 0x800228A2,
+		0x20A00A08, 0x80282088, 0x800800A2, 0x08028228
+	};
+
+	static bool IsSmallPrime(int x)
+	{
+		if (x < 2 || x >= 256) {
+			return false;
+		}
+		return ((SMALL_PRIMES_BF[x >> 5] >> (x & 31)) & (uint)1) != 0;
+	}
+
+	/*
+	 * Test an integer for primality. This function runs up to 50
+	 * Miller-Rabin rounds, which is a lot of overkill but ensures
+	 * that non-primes will be reliably detected (with overwhelming
+	 * probability) even with maliciously crafted inputs. "Normal"
+	 * non-primes will be detected most of the time at the first
+	 * iteration.
+	 *
+	 * This function is not constant-time.
+	 */
+	public static bool IsPrime(byte[] x)
+	{
+		x = NormalizeBE(x);
+
+		/*
+		 * Handle easy cases:
+		 *   0 is not prime
+		 *   small primes (one byte) are known in a constant bit-field
+		 *   even numbers (larger than one byte) are non-primes
+		 */
+		if (x.Length == 0) {
+			return false;
+		}
+		if (x.Length == 1) {
+			return IsSmallPrime(x[0]);
+		}
+		if ((x[x.Length - 1] & 0x01) == 0) {
+			return false;
+		}
+
+		/*
+		 * Perform some trial divisions by small primes.
+		 */
+		for (int sp = 3; sp < 256; sp += 2) {
+			if (!IsSmallPrime(sp)) {
+				continue;
+			}
+			int z = 0;
+			foreach (byte b in x) {
+				z = ((z << 8) + b) % sp;
+			}
+			if (z == 0) {
+				return false;
+			}
+		}
+
+		/*
+		 * Run some Miller-Rabin rounds. We use as basis random
+		 * integers that are one byte smaller than the modulus.
+		 */
+		ModInt xm1 = new ModInt(x);
+		ModInt y = xm1.Dup();
+		y.Set(1);
+		xm1.Sub(y);
+		byte[] e = xm1.Encode();
+		ModInt a = new ModInt(x);
+		byte[] buf = new byte[x.Length - 1];
+		for (int i = 0; i < 50; i ++) {
+			RNG.GetBytes(buf);
+			a.Decode(buf);
+			a.Pow(e);
+			if (!a.IsOne) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	/*
+	 * Right-shift an array of bytes by some bits. The bit count MUST
+	 * be positive or zero. Extra bits are dropped on the right, and
+	 * left positions are filled with zeros.
+	 */
+	public static void RShift(byte[] buf, int numBits)
+	{
+		RShift(buf, 0, buf.Length, numBits);
+	}
+
+	/*
+	 * Right-shift an array of bytes by some bits. The bit count MUST
+	 * be positive or zero. Extra bits are dropped on the right, and
+	 * left positions are filled with zeros.
+	 */
+	public static void RShift(byte[] buf, int off, int len, int numBits)
+	{
+		if (numBits >= 8) {
+			int zlen = numBits >> 3;
+			if (zlen >= len) {
+				for (int i = 0; i < len; i ++) {
+					buf[off + i] = 0;
+				}
+				return;
+			}
+			Array.Copy(buf, off, buf, off + zlen, len - zlen);
+			for (int i = 0; i < zlen; i ++) {
+				buf[off + i] = 0;
+			}
+			off += zlen;
+			len -= zlen;
+			numBits &= 7;
+		}
+
+		int cc = 0;
+		for (int i = 0; i < len; i ++) {
+			int x = buf[off + i];
+			buf[off + i] = (byte)((x >> numBits) + cc);
+			cc = x << (8 - numBits);
+		}
+	}
+}
+
+}
diff --git a/Crypto/BlockCipherCore.cs b/Crypto/BlockCipherCore.cs
new file mode 100644
index 0000000..2f1b761
--- /dev/null
+++ b/Crypto/BlockCipherCore.cs
@@ -0,0 +1,222 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * This class is a convenient base class for implementations of
+ * IBlockCipher. Block cipher implementations must implement:
+ *    int BlockSize { get; }
+ *    void SetKey(byte[] key, int off, int len)
+ *    void BlockEncrypt(byte[] data, int off)
+ *    void BlockDecrypt(byte[] data, int off)
+ *
+ * Note that 'BlockSize' is invoked from the constructor of this class.
+ *
+ * Implementations MAY also override the default implementations of:
+ *    void CBCEncrypt(byte[] iv, byte[] data)
+ *    void CBCEncrypt(byte[] iv, byte[] data, int off, int len)
+ *    void CBCDecrypt(byte[] iv, byte[] data)
+ *    void CBCDecrypt(byte[] iv, byte[] data, int off, int len)
+ *    uint CTRRun(byte[] iv, uint cc, byte[] data)
+ *    uint CTRRun(byte[] iv, uint cc, byte[] data, int off, int len)
+ * Note that CBCEncrypt(byte[],byte[]) (respectively
+ * CBCDecrypt(byte[],byte[]) and CTRRun(byte[],uint,byte[])) simply
+ * calls CBCEncrypt(byte[],byte[],int,int) (respectively
+ * CBCDecrypt(byte[],byte[],int,int) and
+ * CTRRun(byte[],uint,byte[],int,int)) so implementations who wish to
+ * override these methods may content themselves with overriding the two
+ * methods with the "off" and "len" extra parameters.
+ */
+
+public abstract class BlockCipherCore : IBlockCipher {
+
+	byte[] tmp;
+
+	/*
+	 * This constructor invokes 'BlockSize'.
+	 */
+	public BlockCipherCore()
+	{
+		tmp = new byte[BlockSize];
+	}
+
+	/* see IBlockCipher */
+	public abstract int BlockSize { get; }
+
+	/*
+	 * This method is implemented by calling SetKey(byte[],int,int).
+	 */
+	public virtual void SetKey(byte[] key)
+	{
+		SetKey(key, 0, key.Length);
+	}
+
+	/* see IBlockCipher */
+	public abstract void SetKey(byte[] key, int off, int len);
+
+	/*
+	 * This method is implemented by calling BlockEncrypt(byte[],int).
+	 */
+	public virtual void BlockEncrypt(byte[] buf)
+	{
+		BlockEncrypt(buf, 0);
+	}
+
+	/* see IBlockCipher */
+	public abstract void BlockEncrypt(byte[] data, int off);
+
+	/*
+	 * This method is implemented by calling BlockDecrypt(byte[],int).
+	 */
+	public virtual void BlockDecrypt(byte[] buf)
+	{
+		BlockDecrypt(buf, 0);
+	}
+
+	/* see IBlockCipher */
+	public abstract void BlockDecrypt(byte[] data, int off);
+
+	/*
+	 * This method is implemented by calling
+	 * CBCEncrypt(byte[],byte[],int,int).
+	 */
+	public virtual void CBCEncrypt(byte[] iv, byte[] data)
+	{
+		CBCEncrypt(iv, data, 0, data.Length);
+	}
+
+	/* see IBlockCipher */
+	public virtual void CBCEncrypt(
+		byte[] iv, byte[] data, int off, int len)
+	{
+		int blen = BlockSize;
+		if (iv.Length != blen) {
+			throw new CryptoException("wrong IV length");
+		}
+		if (len >= blen) {
+			for (int i = 0; i < blen; i ++) {
+				data[off + i] ^= iv[i];
+			}
+			BlockEncrypt(data, off);
+			off += blen;
+			len -= blen;
+			while (len >= blen) {
+				for (int i = 0; i < blen; i ++) {
+					data[off + i] ^= data[off + i - blen];
+				}
+				BlockEncrypt(data, off);
+				off += blen;
+				len -= blen;
+			}
+		}
+		if (len != 0) {
+			throw new CryptoException("data length is not"
+				+ " multiple of the block size");
+		}
+	}
+
+	/*
+	 * This method is implemented by calling
+	 * CBCDecrypt(byte[],byte[],int,int).
+	 */
+	public virtual void CBCDecrypt(byte[] iv, byte[] data)
+	{
+		CBCDecrypt(iv, data, 0, data.Length);
+	}
+
+	/* see IBlockCipher */
+	public virtual void CBCDecrypt(
+		byte[] iv, byte[] data, int off, int len)
+	{
+		int blen = BlockSize;
+		if (iv.Length != blen) {
+			throw new CryptoException("wrong IV length");
+		}
+		int dblen = blen << 1;
+		off += len;
+		while (len >= dblen) {
+			off -= blen;
+			BlockDecrypt(data, off);
+			for (int i = 0; i < blen; i ++) {
+				data[off + i] ^= data[off + i - blen];
+			}
+			len -= blen;
+		}
+		if (len >= blen) {
+			off -= blen;
+			BlockDecrypt(data, off);
+			for (int i = 0; i < blen; i ++) {
+				data[off + i] ^= iv[i];
+			}
+			len -= blen;
+		}
+		if (len != 0) {
+			throw new CryptoException("data length is not"
+				+ " multiple of the block size");
+		}
+	}
+
+	/*
+	 * This method is implemented by calling
+	 * CTRRun(byte[],uint,byte[],int,int).
+	 */
+	public virtual uint CTRRun(byte[] iv, uint cc, byte[] data)
+	{
+		return CTRRun(iv, cc, data, 0, data.Length);
+	}
+
+	/* see IBlockCipher */
+	public virtual uint CTRRun(
+		byte[] iv, uint cc, byte[] data, int off, int len)
+	{
+		int blen = BlockSize;
+		if (iv.Length != blen - 4) {
+			throw new CryptoException("wrong IV length");
+		}
+		while (len > 0) {
+			Array.Copy(iv, 0, tmp, 0, blen - 4);
+			tmp[blen - 4] = (byte)(cc >> 24);
+			tmp[blen - 3] = (byte)(cc >> 16);
+			tmp[blen - 2] = (byte)(cc >> 8);
+			tmp[blen - 1] = (byte)cc;
+			BlockEncrypt(tmp, 0);
+			int clen = Math.Min(blen, len);
+			for (int i = 0; i < clen; i ++) {
+				data[off + i] ^= tmp[i];
+			}
+			off += clen;
+			len -= clen;
+			cc ++;
+		}
+		return cc;
+	}
+
+	/* see IBlockCipher */
+	public abstract IBlockCipher Dup();
+}
+
+}
diff --git a/Crypto/ChaCha20.cs b/Crypto/ChaCha20.cs
new file mode 100644
index 0000000..52e1fd7
--- /dev/null
+++ b/Crypto/ChaCha20.cs
@@ -0,0 +1,311 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * ChaCha20 implementation. 
+ */
+
+public sealed class ChaCha20 {
+
+	uint k0, k1, k2, k3, k4, k5, k6, k7;
+
+	const uint CW0 = 0x61707865;
+	const uint CW1 = 0x3320646E;
+	const uint CW2 = 0x79622D32;
+	const uint CW3 = 0x6B206574;
+
+	/*
+	 * Initialize a new instance.
+	 */
+	public ChaCha20()
+	{
+	}
+
+	/*
+	 * Set the key (32 bytes).
+	 */
+	public void SetKey(byte[] key)
+	{
+		SetKey(key, 0, key.Length);
+	}
+
+	/*
+	 * Set the key (32 bytes).
+	 */
+	public void SetKey(byte[] key, int off, int len)
+	{
+		if (len != 32) {
+			throw new ArgumentException(
+				"bad ChaCha20 key length: " + len);
+		}
+		k0 = Dec32le(key, off +  0);
+		k1 = Dec32le(key, off +  4);
+		k2 = Dec32le(key, off +  8);
+		k3 = Dec32le(key, off + 12);
+		k4 = Dec32le(key, off + 16);
+		k5 = Dec32le(key, off + 20);
+		k6 = Dec32le(key, off + 24);
+		k7 = Dec32le(key, off + 28);
+	}
+
+	/*
+	 * Encrypt (or decrypt) some bytes. The current block counter
+	 * is provided, and the new block counter value is returned.
+	 * Each block is 64 bytes; if the data length is not a multiple
+	 * of 64, then the extra bytes from the last block are dropped;
+	 * thus, a long stream of bytes can be encrypted or decrypted
+	 * in several calls, as long as all calls (except possibly the
+	 * last) provide a length that is a multiple of 64.
+	 *
+	 * IV must be exactly 12 bytes.
+	 */
+	public uint Run(byte[] iv, uint cc, byte[] data)
+	{
+		return Run(iv, cc, data, 0, data.Length);
+	}
+
+	/*
+	 * Encrypt (or decrypt) some bytes. The current block counter
+	 * is provided, and the new block counter value is returned.
+	 * Each block is 64 bytes; if the data length is not a multiple
+	 * of 64, then the extra bytes from the last block are dropped;
+	 * thus, a long stream of bytes can be encrypted or decrypted
+	 * in several calls, as long as all calls (except possibly the
+	 * last) provide a length that is a multiple of 64.
+	 *
+	 * IV must be exactly 12 bytes.
+	 */
+	public uint Run(byte[] iv, uint cc, byte[] data, int off, int len)
+	{
+		uint iv0 = Dec32le(iv, 0);
+		uint iv1 = Dec32le(iv, 4);
+		uint iv2 = Dec32le(iv, 8);
+		while (len > 0) {
+			uint s0, s1, s2, s3, s4, s5, s6, s7;
+			uint s8, s9, sA, sB, sC, sD, sE, sF;
+
+			s0 = CW0;
+			s1 = CW1;
+			s2 = CW2;
+			s3 = CW3;
+			s4 = k0;
+			s5 = k1;
+			s6 = k2;
+			s7 = k3;
+			s8 = k4;
+			s9 = k5;
+			sA = k6;
+			sB = k7;
+			sC = cc;
+			sD = iv0;
+			sE = iv1;
+			sF = iv2;
+
+			for (int i = 0; i < 10; i ++) {
+				s0 += s4;
+				sC ^= s0;
+				sC = (sC << 16) | (sC >> 16);
+				s8 += sC;
+				s4 ^= s8;
+				s4 = (s4 << 12) | (s4 >> 20);
+				s0 += s4;
+				sC ^= s0;
+				sC = (sC <<  8) | (sC >> 24);
+				s8 += sC;
+				s4 ^= s8;
+				s4 = (s4 <<  7) | (s4 >> 25);
+
+				s1 += s5;
+				sD ^= s1;
+				sD = (sD << 16) | (sD >> 16);
+				s9 += sD;
+				s5 ^= s9;
+				s5 = (s5 << 12) | (s5 >> 20);
+				s1 += s5;
+				sD ^= s1;
+				sD = (sD <<  8) | (sD >> 24);
+				s9 += sD;
+				s5 ^= s9;
+				s5 = (s5 <<  7) | (s5 >> 25);
+
+				s2 += s6;
+				sE ^= s2;
+				sE = (sE << 16) | (sE >> 16);
+				sA += sE;
+				s6 ^= sA;
+				s6 = (s6 << 12) | (s6 >> 20);
+				s2 += s6;
+				sE ^= s2;
+				sE = (sE <<  8) | (sE >> 24);
+				sA += sE;
+				s6 ^= sA;
+				s6 = (s6 <<  7) | (s6 >> 25);
+
+				s3 += s7;
+				sF ^= s3;
+				sF = (sF << 16) | (sF >> 16);
+				sB += sF;
+				s7 ^= sB;
+				s7 = (s7 << 12) | (s7 >> 20);
+				s3 += s7;
+				sF ^= s3;
+				sF = (sF <<  8) | (sF >> 24);
+				sB += sF;
+				s7 ^= sB;
+				s7 = (s7 <<  7) | (s7 >> 25);
+
+				s0 += s5;
+				sF ^= s0;
+				sF = (sF << 16) | (sF >> 16);
+				sA += sF;
+				s5 ^= sA;
+				s5 = (s5 << 12) | (s5 >> 20);
+				s0 += s5;
+				sF ^= s0;
+				sF = (sF <<  8) | (sF >> 24);
+				sA += sF;
+				s5 ^= sA;
+				s5 = (s5 <<  7) | (s5 >> 25);
+
+				s1 += s6;
+				sC ^= s1;
+				sC = (sC << 16) | (sC >> 16);
+				sB += sC;
+				s6 ^= sB;
+				s6 = (s6 << 12) | (s6 >> 20);
+				s1 += s6;
+				sC ^= s1;
+				sC = (sC <<  8) | (sC >> 24);
+				sB += sC;
+				s6 ^= sB;
+				s6 = (s6 <<  7) | (s6 >> 25);
+
+				s2 += s7;
+				sD ^= s2;
+				sD = (sD << 16) | (sD >> 16);
+				s8 += sD;
+				s7 ^= s8;
+				s7 = (s7 << 12) | (s7 >> 20);
+				s2 += s7;
+				sD ^= s2;
+				sD = (sD <<  8) | (sD >> 24);
+				s8 += sD;
+				s7 ^= s8;
+				s7 = (s7 <<  7) | (s7 >> 25);
+
+				s3 += s4;
+				sE ^= s3;
+				sE = (sE << 16) | (sE >> 16);
+				s9 += sE;
+				s4 ^= s9;
+				s4 = (s4 << 12) | (s4 >> 20);
+				s3 += s4;
+				sE ^= s3;
+				sE = (sE <<  8) | (sE >> 24);
+				s9 += sE;
+				s4 ^= s9;
+				s4 = (s4 <<  7) | (s4 >> 25);
+			}
+
+			s0 += CW0;
+			s1 += CW1;
+			s2 += CW2;
+			s3 += CW3;
+			s4 += k0;
+			s5 += k1;
+			s6 += k2;
+			s7 += k3;
+			s8 += k4;
+			s9 += k5;
+			sA += k6;
+			sB += k7;
+			sC += cc;
+			sD += iv0;
+			sE += iv1;
+			sF += iv2;
+
+			int limit = off + len;
+			Xor32le(s0, data, off +  0, limit);
+			Xor32le(s1, data, off +  4, limit);
+			Xor32le(s2, data, off +  8, limit);
+			Xor32le(s3, data, off + 12, limit);
+			Xor32le(s4, data, off + 16, limit);
+			Xor32le(s5, data, off + 20, limit);
+			Xor32le(s6, data, off + 24, limit);
+			Xor32le(s7, data, off + 28, limit);
+			Xor32le(s8, data, off + 32, limit);
+			Xor32le(s9, data, off + 36, limit);
+			Xor32le(sA, data, off + 40, limit);
+			Xor32le(sB, data, off + 44, limit);
+			Xor32le(sC, data, off + 48, limit);
+			Xor32le(sD, data, off + 52, limit);
+			Xor32le(sE, data, off + 56, limit);
+			Xor32le(sF, data, off + 60, limit);
+
+			off += 64;
+			len -= 64;
+			cc ++;
+		}
+		return cc;
+	}
+
+	static uint Dec32le(byte[] buf, int off)
+	{
+		return (uint)buf[off]
+			| ((uint)buf[off + 1] << 8)
+			| ((uint)buf[off + 2] << 16)
+			| ((uint)buf[off + 3] << 24);
+	}
+
+	static void Xor32le(uint x, byte[] buf, int off, int limit)
+	{
+		if (off + 4 <= limit) {
+			buf[off] ^= (byte)x;
+			buf[off + 1] ^= (byte)(x >> 8);
+			buf[off + 2] ^= (byte)(x >> 16);
+			buf[off + 3] ^= (byte)(x >> 24);
+		} else {
+			if (off + 2 <= limit) {
+				if (off + 3 <= limit) {
+					buf[off] ^= (byte)x;
+					buf[off + 1] ^= (byte)(x >> 8);
+					buf[off + 2] ^= (byte)(x >> 16);
+				} else {
+					buf[off] ^= (byte)x;
+					buf[off + 1] ^= (byte)(x >> 8);
+				}
+			} else {
+				if (off + 1 <= limit) {
+					buf[off] ^= (byte)x;
+				}
+			}
+		}
+	}
+}
+
+}
diff --git a/Crypto/CryptoException.cs b/Crypto/CryptoException.cs
new file mode 100644
index 0000000..5ce93c6
--- /dev/null
+++ b/Crypto/CryptoException.cs
@@ -0,0 +1,45 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * A basic exception class, to be thrown when a cryptographic
+ * algorithm encounters an unfixable issue.
+ */
+
+public class CryptoException : Exception {
+
+	public CryptoException(string msg) : base(msg)
+	{
+	}
+
+	public CryptoException(string msg, Exception e) : base(msg, e)
+	{
+	}
+}
+
+}
diff --git a/Crypto/DES.cs b/Crypto/DES.cs
new file mode 100644
index 0000000..3446d58
--- /dev/null
+++ b/Crypto/DES.cs
@@ -0,0 +1,492 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * Perfunctory implementation of the Triple-DES block cipher (also works
+ * as "single DES" when used with an 8-byte DES key). It only processes
+ * single blocks, and does it "in place" (the input block is replaced
+ * with the output block in the same array). This code is a direct
+ * translation of the specification and is not optimized for speed.
+ *
+ * Supported key sizes are 8, 16 and 24 bytes. When an 8-byte key is
+ * used, this is the original DES (64-bit key, out of which 8 are
+ * unused, so the "true key size" is 56 bits). When a 16-byte or 24-byte
+ * key is used, this is Triple-DES (with a 16-byte key, the key is
+ * expanded to 24 bytes by reusing bytes 0..7 as bytes 16..23 of the
+ * key).
+ *
+ * Instances are not thread-safe; however, distinct threads may use
+ * distinct instances concurrently.
+ *
+ * Since instances are pure managed code, there is no need for explicit
+ * disposal after usage.
+ */
+
+public sealed class DES : BlockCipherCore {
+
+	ulong[] skey;
+	int rounds;
+
+	/*
+	 * Initialize a new instance.
+	 */
+	public DES()
+	{
+		skey = new ulong[48];
+	}
+
+	/* see IBlockCipher */
+	public override IBlockCipher Dup()
+	{
+		DES d = new DES();
+		Array.Copy(skey, 0, d.skey, 0, skey.Length);
+		d.rounds = rounds;
+		return d;
+	}
+
+	/*
+	 * Get the block size in bytes (always 8).
+	 */
+	public override int BlockSize {
+		get {
+			return 8;
+		}
+	}
+
+	/*
+	 * Set the key (8, 16 or 24 bytes).
+	 */
+	public override void SetKey(byte[] key, int off, int len)
+	{
+		switch (len) {
+		case 8:
+			KeySchedule(key, off, 0);
+			rounds = 1;
+			break;
+		case 16:
+			KeySchedule(key, off, 0);
+			KeySchedule(key, off + 8, 16);
+			KeySchedule(key, off, 32);
+			rounds = 3;
+			break;
+		case 24:
+			KeySchedule(key, off, 0);
+			KeySchedule(key, off + 8, 16);
+			KeySchedule(key, off + 16, 32);
+			rounds = 3;
+			break;
+		default:
+			throw new ArgumentException(
+				"bad DES/3DES key length: " + len);
+		}
+
+		/* 
+		 * Inverse order of subkeys for the middle-DES (3DES
+		 * uses the "EDE" configuration).
+		 */
+		for (int j = 16; j < 24; j ++) {
+			ulong w = skey[j];
+			skey[j] = skey[47 - j];
+			skey[47 - j] = w;
+		}
+	}
+
+	void KeySchedule(byte[] key, int off, int skeyOff)
+	{
+		ulong k = Dec64be(key, off);
+		k = Perm(tabPC1, k);
+		ulong kl = k >> 28;
+		ulong kr = k & 0x0FFFFFFF;
+		for (int i = 0; i < 16; i ++) {
+			int r = rotK[i];
+			kl = ((kl << r) | (kl >> (28 - r))) & 0x0FFFFFFF;
+			kr = ((kr << r) | (kr >> (28 - r))) & 0x0FFFFFFF;
+			skey[skeyOff + i] = Perm(tabPC2, (kl << 28) | kr);
+		}
+	}
+
+	/*
+	 * Encrypt one block; the block consists in the 8 bytes beginning
+	 * at offset 'off'. Other bytes in the array are unaltered.
+	 */
+	public override void BlockEncrypt(byte[] buf, int off)
+	{
+		if (rounds == 0) {
+			throw new Exception("no key provided");
+		}
+		ulong x = Dec64be(buf, off);
+		x = DoIP(x);
+		uint xl = (uint)(x >> 32);
+		uint xr = (uint)x;
+		for (int i = 0, k = 0; i < rounds; i ++) {
+			xl ^= FConf(xr, skey[k ++]);
+			xr ^= FConf(xl, skey[k ++]);
+			xl ^= FConf(xr, skey[k ++]);
+			xr ^= FConf(xl, skey[k ++]);
+			xl ^= FConf(xr, skey[k ++]);
+			xr ^= FConf(xl, skey[k ++]);
+			xl ^= FConf(xr, skey[k ++]);
+			xr ^= FConf(xl, skey[k ++]);
+			xl ^= FConf(xr, skey[k ++]);
+			xr ^= FConf(xl, skey[k ++]);
+			xl ^= FConf(xr, skey[k ++]);
+			xr ^= FConf(xl, skey[k ++]);
+			xl ^= FConf(xr, skey[k ++]);
+			xr ^= FConf(xl, skey[k ++]);
+			xl ^= FConf(xr, skey[k ++]);
+			uint tmp = xr ^ FConf(xl, skey[k ++]);
+			xr = xl;
+			xl = tmp;
+		}
+		x = ((ulong)xl << 32) | (ulong)xr;
+		x = DoIPInv(x);
+		Enc64be(x, buf, off);
+	}
+
+	/*
+	 * Decrypt one block; the block consists in the 8 bytes beginning
+	 * at offset 'off'. Other bytes in the array are unaltered.
+	 */
+	public override void BlockDecrypt(byte[] buf, int off)
+	{
+		if (rounds == 0) {
+			throw new Exception("no key provided");
+		}
+		ulong x = Dec64be(buf, off);
+		x = DoIP(x);
+		uint xl = (uint)(x >> 32);
+		uint xr = (uint)x;
+		for (int i = 0, k = rounds << 4; i < rounds; i ++) {
+			xl ^= FConf(xr, skey[-- k]);
+			xr ^= FConf(xl, skey[-- k]);
+			xl ^= FConf(xr, skey[-- k]);
+			xr ^= FConf(xl, skey[-- k]);
+			xl ^= FConf(xr, skey[-- k]);
+			xr ^= FConf(xl, skey[-- k]);
+			xl ^= FConf(xr, skey[-- k]);
+			xr ^= FConf(xl, skey[-- k]);
+			xl ^= FConf(xr, skey[-- k]);
+			xr ^= FConf(xl, skey[-- k]);
+			xl ^= FConf(xr, skey[-- k]);
+			xr ^= FConf(xl, skey[-- k]);
+			xl ^= FConf(xr, skey[-- k]);
+			xr ^= FConf(xl, skey[-- k]);
+			xl ^= FConf(xr, skey[-- k]);
+			uint tmp = xr ^ FConf(xl, skey[-- k]);
+			xr = xl;
+			xl = tmp;
+		}
+		x = ((ulong)xl << 32) | (ulong)xr;
+		x = DoIPInv(x);
+		Enc64be(x, buf, off);
+	}
+
+	/*
+	 * Arrays below are extracted exactly from FIPS 46-3. They use
+	 * the conventions defined in that document:
+	 *
+	 * -- Bits are numbered from 1, in the left-to-right order; in
+	 *    a 64-bit integer, the most significant (leftmost) bit is 1,
+	 *    while the least significant (rightmost) bit is 64.
+	 *
+	 * -- For each permutation (or extraction), the defined array
+	 *    lists the source index of each bit.
+	 *
+	 * -- For the S-boxes, bits 1 (leftmost) and 6 (rightmost) select
+	 *    the row, and bits 2 to 5 select the index within the row.
+	 */
+
+	static uint[,] Sdef = {
+		{
+			14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7,
+			0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8,
+			4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0,
+			15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13
+		}, {
+			15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10,
+			3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5,
+			0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15,
+			13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9
+		}, {
+			10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8,
+			13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1,
+			13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7,
+			1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12
+		}, {
+			7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15,
+			13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9,
+			10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4,
+			3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14
+		}, {
+			2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9,
+			14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6,
+			4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14,
+			11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3
+		}, {
+			12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11,
+			10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8,
+			9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6,
+			4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13
+		}, {
+			4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1,
+			13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6,
+			1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2,
+			6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12
+		}, {
+			13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7,
+			1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2,
+			7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8,
+			2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11
+		}
+	};
+
+	static int[] defP = {
+		16, 7, 20, 21, 29, 12, 28, 17, 1, 15, 23, 26, 5, 18, 31, 10,
+		2, 8, 24, 14, 32, 27, 3, 9, 19, 13, 30, 6, 22, 11, 4, 25
+	};
+
+	static int[] defPC1 = {
+		57, 49, 41, 33, 25, 17, 9,
+		1, 58, 50, 42, 34, 26, 18,
+		10, 2, 59, 51, 43, 35, 27,
+		19, 11, 3, 60, 52, 44, 36,
+		63, 55, 47, 39, 31, 23, 15,
+		7, 62, 54, 46, 38, 30, 22,
+		14, 6, 61, 53, 45, 37, 29,
+		21, 13, 5, 28, 20, 12, 4
+	};
+
+	static int[] defPC2 = {
+		14, 17, 11, 24, 1, 5,
+		3, 28, 15, 6, 21, 10,
+		23, 19, 12, 4, 26, 8,
+		16, 7, 27, 20, 13, 2,
+		41, 52, 31, 37, 47, 55,
+		30, 40, 51, 45, 33, 48,
+		44, 49, 39, 56, 34, 53,
+		46, 42, 50, 36, 29, 32
+	};
+
+	static int[] rotK = {
+		1, 1, 2, 2, 2, 2, 2, 2,
+		1, 2, 2, 2, 2, 2, 2, 1
+	};
+
+	/*
+	 * Permutations (and extractions) are implemented with the
+	 * following tables, that are initialized from the class
+	 * initialization code. The representation of a permutation is
+	 * an array of words, where word at index k is a one-bit mask
+	 * that identifies the source bit that should go at position k
+	 * in the output. The "k" index here is in right-to-left
+	 * convention: least significant bit (rightmost) is numbered 0.
+	 *
+	 * The Perm() method applies a permutation, using one of the
+	 * tab*[] arrays.
+	 *
+	 * The S*[] arrays contain the S-boxes, with the permutation P
+	 * effect merged in, and expecting their 6-bit input "as is".
+	 */
+	static ulong[] tabPC1;
+	static ulong[] tabPC2;
+
+	static uint[] S1, S2, S3, S4, S5, S6, S7, S8;
+
+	static DES()
+	{
+		tabPC1 = new ulong[defPC1.Length];
+		for (int i = 0; i < defPC1.Length; i ++) {
+			tabPC1[55 - i] = (ulong)1 << (64 - defPC1[i]);
+		}
+		tabPC2 = new ulong[defPC2.Length];
+		for (int i = 0; i < defPC2.Length; i ++) {
+			tabPC2[47 - i] = (ulong)1 << (56 - defPC2[i]);
+		}
+
+		int[] PInv = new int[32];
+		for (int i = 0; i < defP.Length; i ++) {
+			PInv[32 - defP[i]] = 31 - i;
+		}
+		S1 = MakeSbox(PInv, 0);
+		S2 = MakeSbox(PInv, 1);
+		S3 = MakeSbox(PInv, 2);
+		S4 = MakeSbox(PInv, 3);
+		S5 = MakeSbox(PInv, 4);
+		S6 = MakeSbox(PInv, 5);
+		S7 = MakeSbox(PInv, 6);
+		S8 = MakeSbox(PInv, 7);
+	}
+
+	static uint[] MakeSbox(int[] PInv, int i)
+	{
+		uint[] S = new uint[64];
+		for (int j = 0; j < 64; j ++) {
+			int idx = ((j & 0x01) << 4) | (j & 0x20)
+				| ((j & 0x1E) >> 1);
+			uint zb = Sdef[i, idx];
+			uint za = 0;
+			for (int k = 0; k < 4; k ++) {
+				za |= ((zb >> k) & 1)
+					<< PInv[((7 - i) << 2) + k];
+			}
+			S[j] = za;
+		}
+		return S;
+	}
+
+	static ulong IPStep(ulong x, int size, ulong mask)
+	{
+		uint left = (uint)(x >> 32);
+		uint right = (uint)x;
+		uint tmp = ((left >> size) ^ right) & (uint)mask;
+		right ^= tmp;
+		left ^= tmp << size;
+		return ((ulong)left << 32) | (ulong)right;
+	}
+
+	static ulong DoIP(ulong x)
+	{
+		/*
+		 * Permutation algorithm is initially from Richard
+		 * Outerbridge; this implementation has been adapted
+		 * from Crypto++ "des.cpp" file (which is in public
+		 * domain).
+		 */
+		uint l = (uint)(x >> 32);
+		uint r = (uint)x;
+		uint t;
+		t = ((l >>  4) ^ r) & 0x0F0F0F0F;
+		r ^= t;
+		l ^= t <<  4;
+		t = ((l >> 16) ^ r) & 0x0000FFFF;
+		r ^= t;
+		l ^= t << 16;
+		t = ((r >>  2) ^ l) & 0x33333333;
+		l ^= t;
+		r ^= t <<  2;
+		t = ((r >>  8) ^ l) & 0x00FF00FF;
+		l ^= t;
+		r ^= t <<  8;
+		t = ((l >>  1) ^ r) & 0x55555555;
+		r ^= t;
+		l ^= t <<  1;
+		x = ((ulong)l << 32) | (ulong)r;
+		return x;
+	}
+
+	static ulong DoIPInv(ulong x)
+	{
+		/*
+		 * See DoIP().
+		 */
+		uint l = (uint)(x >> 32);
+		uint r = (uint)x;
+		uint t;
+		t = ((l >>  1) ^ r) & 0x55555555;
+		r ^= t;
+		l ^= t <<  1;
+		t = ((r >>  8) ^ l) & 0x00FF00FF;
+		l ^= t;
+		r ^= t <<  8;
+		t = ((r >>  2) ^ l) & 0x33333333;
+		l ^= t;
+		r ^= t <<  2;
+		t = ((l >> 16) ^ r) & 0x0000FFFF;
+		r ^= t;
+		l ^= t << 16;
+		t = ((l >>  4) ^ r) & 0x0F0F0F0F;
+		r ^= t;
+		l ^= t <<  4;
+		x = ((ulong)l << 32) | (ulong)r;
+		return x;
+	}
+
+	/*
+	 * Apply a permutation or extraction. For all k, bit k of the
+	 * output (right-to-left numbering) is set if and only if the
+	 * source bit in x defined by the tab[k] mask is set.
+	 */
+	static ulong Perm(ulong[] tab, ulong x)
+	{
+		ulong y = 0;
+		for (int i = 0; i < tab.Length; i ++) {
+			if ((x & tab[i]) != 0) {
+				y |= (ulong)1 << i;
+			}
+		}
+		return y;
+	}
+
+	static uint FConf(uint r0, ulong sk)
+	{
+		uint skhi = (uint)(sk >> 24);
+		uint sklo = (uint)sk;
+		uint r1 = (r0 >> 16) | (r0 << 16);
+		return
+			  S1[((r1 >> 11) ^ (skhi >> 18)) & 0x3F]
+			| S2[((r0 >> 23) ^ (skhi >> 12)) & 0x3F]
+			| S3[((r0 >> 19) ^ (skhi >>  6)) & 0x3F]
+			| S4[((r0 >> 15) ^ (skhi      )) & 0x3F]
+			| S5[((r0 >> 11) ^ (sklo >> 18)) & 0x3F]
+			| S6[((r0 >>  7) ^ (sklo >> 12)) & 0x3F]
+			| S7[((r0 >>  3) ^ (sklo >>  6)) & 0x3F]
+			| S8[((r1 >> 15) ^ (sklo      )) & 0x3F];
+	}
+
+	/*
+	 * 64-bit big-endian decoding.
+	 */
+	static ulong Dec64be(byte[] buf, int off)
+	{
+		return ((ulong)buf[off] << 56)
+			| ((ulong)buf[off + 1] << 48)
+			| ((ulong)buf[off + 2] << 40)
+			| ((ulong)buf[off + 3] << 32)
+			| ((ulong)buf[off + 4] << 24)
+			| ((ulong)buf[off + 5] << 16)
+			| ((ulong)buf[off + 6] << 8)
+			| (ulong)buf[off + 7];
+	}
+
+	/*
+	 * 64-bit big-endian encoding.
+	 */
+	static void Enc64be(ulong v, byte[] buf, int off)
+	{
+		buf[off + 0] = (byte)(v >> 56);
+		buf[off + 1] = (byte)(v >> 48);
+		buf[off + 2] = (byte)(v >> 40);
+		buf[off + 3] = (byte)(v >> 32);
+		buf[off + 4] = (byte)(v >> 24);
+		buf[off + 5] = (byte)(v >> 16);
+		buf[off + 6] = (byte)(v >> 8);
+		buf[off + 7] = (byte)v;
+	}
+}
+
+}
diff --git a/Crypto/DSAUtils.cs b/Crypto/DSAUtils.cs
new file mode 100644
index 0000000..c58cf90
--- /dev/null
+++ b/Crypto/DSAUtils.cs
@@ -0,0 +1,322 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * This class contains some utility methods used for DSA and ECDSA.
+ */
+
+public class DSAUtils {
+
+	/*
+	 * Convert an ASN.1 DSA signature to "raw" format. A "raw" signature
+	 * is the concatenation of two unsigned big-endian integers of
+	 * the same length. An ASN.1 signature is a DER-encoded SEQUENCE
+	 * of two INTEGER values. The returned signature will have the
+	 * minimum length that can hold the two signature elements; use
+	 * SigRawNormalize() to adjust that length.
+	 *
+	 * If the source signature is syntaxically invalid (not valid DER),
+	 * then null is returned.
+	 */
+	public static byte[] SigAsn1ToRaw(byte[] sig)
+	{
+		return SigAsn1ToRaw(sig, 0, sig.Length);
+	}
+
+	public static byte[] SigAsn1ToRaw(byte[] sig, int off, int len)
+	{
+		int lim = off + len;
+		if (len <= 2 || sig[off ++] != 0x30) {
+			return null;
+		}
+		int tlen = DecodeLength(sig, ref off, lim);
+		if (tlen != (lim - off)) {
+			return null;
+		}
+		int roff, rlen;
+		int soff, slen;
+		if (!DecodeInteger(sig, ref off, lim, out roff, out rlen)) {
+			return null;
+		}
+		if (!DecodeInteger(sig, ref off, lim, out soff, out slen)) {
+			return null;
+		}
+		if (off != lim) {
+			return null;
+		}
+		int ulen = Math.Max(rlen, slen);
+		byte[] raw = new byte[ulen << 1];
+		Array.Copy(sig, roff, raw, ulen - rlen, rlen);
+		Array.Copy(sig, soff, raw, (ulen << 1) - slen, slen);
+		return raw;
+	}
+
+	static int DecodeLength(byte[] buf, ref int off, int lim)
+	{
+		if (off >= lim) {
+			return -1;
+		}
+		int fb = buf[off ++];
+		if (fb < 0x80) {
+			return fb;
+		}
+		int elen = fb - 0x80;
+		if (elen == 0) {
+			return -1;
+		}
+		int acc = 0;
+		while (elen -- > 0) {
+			if (off >= lim) {
+				return -1;
+			}
+			if (acc > 0x7FFFFF) {
+				return -1;
+			}
+			acc = (acc << 8) + buf[off ++];
+		}
+		return acc;
+	}
+
+	static bool DecodeInteger(byte[] buf, ref int off, int lim,
+		out int voff, out int vlen)
+	{
+		voff = -1;
+		vlen = -1;
+		if (off >= lim || buf[off ++] != 0x02) {
+			return false;
+		}
+		int len = DecodeLength(buf, ref off, lim);
+		if (len <= 0 || len > (lim - off)) {
+			return false;
+		}
+		voff = off;
+		vlen = len;
+		off += len;
+		while (vlen > 1 && buf[voff] == 0x00) {
+			voff ++;
+			vlen --;
+		}
+		return true;
+	}
+
+	/*
+	 * Reduce a "raw" signature to its minimal length. The minimal
+	 * length depends on the values of the inner elements; normally,
+	 * that length is equal to twice the length of the encoded
+	 * subgroup order, but it can be shorter by a few bytes
+	 * (occasionally by two bytes; shorter signatures are very
+	 * rare).
+	 *
+	 * If the source signature is null or has an odd length, then
+	 * null is returned. If the source signature already has
+	 * minimal length, then it is returned as is. Otherwise,
+	 * a new array is created with the minimal length, filled,
+	 * and returned.
+	 */
+	public static byte[] SigRawMinimalize(byte[] sigRaw)
+	{
+		int minLen = GetMinRawLength(sigRaw);
+		if (minLen <= 0) {
+			return null;
+		}
+		if (minLen == sigRaw.Length) {
+			return sigRaw;
+		}
+		int m = sigRaw.Length >> 1;
+		int lh = minLen >> 1;
+		byte[] sig = new byte[lh + lh];
+		Array.Copy(sigRaw, m - lh, sig, 0, lh);
+		Array.Copy(sigRaw, m + m - lh, sig, lh, lh);
+		return sig;
+	}
+
+	/*
+	 * Normalize a "raw" signature to the specified length. If
+	 * the source array already has the right length, then it is
+	 * returned as is. Otherwise, a new array is created with the
+	 * requested length, and filled with the signature elements.
+	 *
+	 * If the source signature is null, or has an odd length, then
+	 * null is returned. If the requested length is not valid (odd
+	 * length) or cannot be achieved (because the signature elements
+	 * are too large), then null is returned.
+	 */
+	public static byte[] SigRawNormalize(byte[] sigRaw, int len)
+	{
+		int minLen = GetMinRawLength(sigRaw);
+		if (minLen <= 0) {
+			return null;
+		}
+		if ((len & 1) != 0) {
+			return null;
+		}
+		int hlen = len >> 1;
+		if (sigRaw.Length == len) {
+			return sigRaw;
+		}
+		int m = sigRaw.Length >> 1;
+		int lh = minLen >> 1;
+		byte[] sig = new byte[len];
+		Array.Copy(sigRaw, m - lh, sig, hlen - lh, lh);
+		Array.Copy(sigRaw, m + m - lh, sig, len - lh, lh);
+		return sig;
+	}
+
+	static int GetMinRawLength(byte[] sig)
+	{
+		if (sig == null || (sig.Length & 1) != 0) {
+			return -1;
+		}
+		int m = sig.Length << 1;
+		int lr, ls;
+		for (lr = m; lr > 0; lr --) {
+			if (sig[m - lr] != 0) {
+				break;
+			}
+		}
+		for (ls = m; ls > 0; ls --) {
+			if (sig[m + m - ls] != 0) {
+				break;
+			}
+		}
+		return Math.Max(lr, ls) << 1;
+	}
+
+	/*
+	 * Convert a "raw" DSA signature to ASN.1. A "raw" signature
+	 * is the concatenation of two unsigned big-endian integers of
+	 * the same length. An ASN.1 signature is a DER-encoded SEQUENCE
+	 * of two INTEGER values.
+	 *
+	 * If the source signature is syntaxically invalid (zero length,
+	 * or odd length), then null is returned.
+	 */
+	public static byte[] SigRawToAsn1(byte[] sig)
+	{
+		return SigRawToAsn1(sig, 0, sig.Length);
+	}
+
+	public static byte[] SigRawToAsn1(byte[] sig, int off, int len)
+	{
+		if (len <= 0 || (len & 1) != 0) {
+			return null;
+		}
+		int tlen = len >> 1;
+		int rlen = LengthOfInteger(sig, off, tlen);
+		int slen = LengthOfInteger(sig, off + tlen, tlen);
+		int ulen = 1 + LengthOfLength(rlen) + rlen
+			+ 1 + LengthOfLength(slen) + slen;
+		byte[] s = new byte[1 + LengthOfLength(ulen) + ulen];
+		int k = 0;
+		s[k ++] = 0x30;
+		k += EncodeLength(ulen, s, k);
+		k += EncodeInteger(sig, off, tlen, s, k);
+		k += EncodeInteger(sig, off + tlen, tlen, s, k);
+		// DEBUG
+		if (k != s.Length) {
+			throw new Exception("DSA internal error");
+		}
+		return s;
+	}
+
+	/*
+	 * Get the length of the value of an INTEGER containing a
+	 * specified value. Returned length includes the leading 0x00
+	 * byte (if applicable) but not the tag or length fields.
+	 */
+	static int LengthOfInteger(byte[] x, int off, int len)
+	{
+		while (len > 0 && x[off] == 0) {
+			off ++;
+			len --;
+		}
+		if (len == 0) {
+			return 1;
+		}
+		return (x[off] >= 0x80) ? len + 1 : len;
+	}
+
+	static int LengthOfLength(int len)
+	{
+		if (len < 0x80) {
+			return 1;
+		} else if (len < 0x100) {
+			return 2;
+		} else if (len < 0x10000) {
+			return 3;
+		} else if (len < 0x1000000) {
+			return 4;
+		} else {
+			return 5;
+		}
+	}
+
+	static int EncodeLength(int len, byte[] dst, int off)
+	{
+		if (len < 0x80) {
+			dst[off] = (byte)len;
+			return 1;
+		}
+		int k = 0;
+		for (int z = len; z != 0; z >>= 8) {
+			k ++;
+		}
+		dst[off] = (byte)(0x80 + k);
+		for (int i = 0; i < k; i ++) {
+			dst[off + k - i] = (byte)(len >> (i << 3));
+		}
+		return k + 1;
+	}
+
+	static int EncodeInteger(byte[] x, int off, int len,
+		byte[] dst, int dstOff)
+	{
+		int orig = dstOff;
+		dst[dstOff ++] = 0x02;
+		while (len > 0 && x[off] == 0) {
+			off ++;
+			len --;
+		}
+		if (len == 0) {
+			dst[dstOff ++] = 0x01;
+			dst[dstOff ++] = 0x00;
+			return dstOff - orig;
+		}
+		if (x[off] >= 0x80) {
+			dstOff += EncodeLength(len + 1, dst, dstOff);
+			dst[dstOff ++] = 0x00;
+		} else {
+			dstOff += EncodeLength(len, dst, dstOff);
+		}
+		Array.Copy(x, off, dst, dstOff, len);
+		dstOff += len;
+		return dstOff - orig;
+	}
+}
+
+}
diff --git a/Crypto/DigestCore.cs b/Crypto/DigestCore.cs
new file mode 100644
index 0000000..49171f0
--- /dev/null
+++ b/Crypto/DigestCore.cs
@@ -0,0 +1,130 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * This class is a convenient base class for implementations of IDigest.
+ * Hash function implementations must implement:
+ *    int DigestSize { get; }
+ *    int BlockSize { get; }
+ *    int PaddingOverhead { get; }
+ *    void Update(byte b)
+ *    void Update(byte[] buf, int off, int len)
+ *    void DoPartial(byte[] outBuf, int off)
+ *    void Reset()
+ *    IDigest Dup()
+ *
+ * Implementations SHOULD provide overrides for:
+ *    void CurrentState(byte[], int)
+ *
+ * In this class:
+ *    Update(byte[]) calls Update(byte[],int,int)
+ *    DoPartial() calls DoPartial(byte[],int)
+ *    DoFinal() calls DoPartial() and Reset()
+ *    DoFinal(byte[],int) calls DoPartial(byte[],int) and Reset()
+ */
+
+public abstract class DigestCore : IDigest {
+
+	/* see IDigest */
+	public abstract string Name { get; }
+
+	/* see IDigest */
+	public abstract int DigestSize { get; }
+
+	/* see IDigest */
+	public abstract int BlockSize { get; }
+
+	/* see IDigest */
+	public abstract void Update(byte b);
+
+	/* see IDigest */
+	public virtual void Update(byte[] buf)
+	{
+		Update(buf, 0, buf.Length);
+	}
+
+	/* see IDigest */
+	public abstract void Update(byte[] buf, int off, int len);
+
+	/* see IDigest */
+	public abstract void DoPartial(byte[] outBuf, int off);
+
+	/* see IDigest */
+	public virtual byte[] DoPartial()
+	{
+		byte[] buf = new byte[DigestSize];
+		DoPartial(buf, 0);
+		return buf;
+	}
+
+	/* see IDigest */
+	public virtual void DoFinal(byte[] outBuf, int off)
+	{
+		DoPartial(outBuf, off);
+		Reset();
+	}
+
+	/* see IDigest */
+	public virtual byte[] DoFinal()
+	{
+		byte[] r = DoPartial();
+		Reset();
+		return r;
+	}
+
+	/* see IDigest */
+	public abstract void Reset();
+
+	/* see IDigest */
+	public abstract IDigest Dup();
+
+	/*
+	 * Default implementation throws a NotSupportedException.
+	 */
+	public virtual void CurrentState(byte[] outBuf, int off)
+	{
+		throw new NotSupportedException();
+	}
+
+	/* see IDigest */
+	public virtual byte[] Hash(byte[] buf)
+	{
+		return Hash(buf, 0, buf.Length);
+	}
+
+	/* see IDigest */
+	public virtual byte[] Hash(byte[] buf, int off, int len)
+	{
+		IDigest h = Dup();
+		h.Reset();
+		h.Update(buf, off, len);
+		return h.DoFinal();
+	}
+}
+
+}
diff --git a/Crypto/EC.cs b/Crypto/EC.cs
new file mode 100644
index 0000000..959644d
--- /dev/null
+++ b/Crypto/EC.cs
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * This class contains static definitions for some standard elliptic
+ * curves.
+ */
+
+public class EC {
+
+	// public static ECCurve P192 = NIST.P192;
+	// public static ECCurve P224 = NIST.P224;
+	public static ECCurve P256 = NIST.P256;
+	public static ECCurve P384 = NIST.P384;
+	public static ECCurve P521 = NIST.P521;
+
+	public static ECCurve Curve25519 = new ECCurve25519();
+
+}
+
+}
diff --git a/Crypto/ECCurve.cs b/Crypto/ECCurve.cs
new file mode 100644
index 0000000..c92e7ec
--- /dev/null
+++ b/Crypto/ECCurve.cs
@@ -0,0 +1,235 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * This class represents an elliptic curve.
+ */
+
+public abstract class ECCurve {
+
+	/*
+	 * Get the subgroup order for this curve (big-endian unsigned
+	 * notation). The subgroup order is supposed to be a prime
+	 * integer.
+	 */
+	public byte[] SubgroupOrder {
+		get {
+			return subgroupOrder;
+		}
+	}
+
+	/*
+	 * Get the cofactor fors this curve (big-endian unsigned notation).
+	 * The cofactor is the quotient of the curve order by the subgroup
+	 * order.
+	 */
+	public byte[] Cofactor {
+		get {
+			return cofactor;
+		}
+	}
+
+	/*
+	 * Get the curve symbolic name.
+	 */
+	public abstract string Name {
+		get;
+	}
+
+	/*
+	 * Get the curve type.
+	 */
+	public abstract ECCurveType CurveType {
+		get;
+	}
+
+	/*
+	 * Get the length (in bytes) of an encoded point. The "point at
+	 * infinity" may use a shorter encoding than other points. This
+	 * uses the "normal" encoding (not compressed).
+	 */
+	public abstract int EncodedLength {
+		get;
+	}
+
+	/*
+	 * Get the length (in bytes) of an encoded compressed point.
+	 * The "point at infinity" may use a shorter encoding than
+	 * other points.
+	 */
+	public abstract int EncodedLengthCompressed {
+		get;
+	}
+
+	/*
+	 * Perform extensive curve validity checks. These tests may be
+	 * computationally expensive.
+	 */
+	public abstract void CheckValid();
+
+	/*
+	 * Get the encoded generator for this curve. This is
+	 * a conventional point that generates the subgroup of prime
+	 * order on which computations are normally done.
+	 */
+	public virtual byte[] GetGenerator(bool compressed)
+	{
+		return MakeGenerator().Encode(compressed);
+	}
+
+	/*
+	 * Get the offset and length of the X coordinate of a point,
+	 * within its encoded representation. When doing a Diffie-Hellman
+	 * key exchange, the resulting shared secret is the X coordinate
+	 * of the resulting point.
+	 */
+	public abstract int GetXoff(out int len);
+
+	/*
+	 * Multiply the provided (encoded) point G by a scalar x. Scalar
+	 * encoding is big-endian. The scalar value shall be non-zero and
+	 * lower than the subgroup order (exception: some curves allow
+	 * larger ranges).
+	 *
+	 * The result is written in the provided D[] array, using either
+	 * compressed or uncompressed format (for some curves, output is
+	 * always compressed). The array shall have the appropriate length.
+	 * Returned value is -1 on success, 0 on error. If 0 is returned
+	 * then the array contents are indeterminate.
+	 *
+	 * G and D need not be distinct arrays.
+	 */
+	public uint Mul(byte[] G, byte[] x, byte[] D, bool compressed)
+	{
+		MutableECPoint P = MakeZero();
+		uint good = P.DecodeCT(G);
+		good &= ~P.IsInfinityCT;
+		good &= P.MulSpecCT(x);
+		good &= P.Encode(D, compressed);
+		return good;
+	}
+
+	/*
+	 * Given points A and B, and scalar x and y, return x*A+y*B. This
+	 * is used for ECDSA. Scalars use big-endian encoding and must be
+	 * non-zero and lower than the subgroup order.
+	 *
+	 * The result is written in the provided D[] array, using either
+	 * compressed or uncompressed format (for some curves, output is
+	 * always compressed). The array shall have the appropriate length.
+	 * Returned value is -1 on success, 0 on error. If 0 is returned
+	 * then the array contents are indeterminate.
+	 *
+	 * Not all curves support this operation; if the curve does not,
+	 * then an exception is thrown.
+	 *
+	 * A, B and D need not be distinct arrays.
+	 */
+	public uint MulAdd(byte[] A, byte[] x, byte[] B, byte[] y,
+		byte[] D, bool compressed)
+	{
+		MutableECPoint P = MakeZero();
+		MutableECPoint Q = MakeZero();
+
+		/*
+		 * Decode both points.
+		 */
+		uint good = P.DecodeCT(A);
+		good &= Q.DecodeCT(B);
+		good &= ~P.IsInfinityCT & ~Q.IsInfinityCT;
+
+		/*
+		 * Perform both point multiplications.
+		 */
+		good &= P.MulSpecCT(x);
+		good &= Q.MulSpecCT(y);
+		good &= ~P.IsInfinityCT & ~Q.IsInfinityCT;
+
+		/*
+		 * Perform addition. The AddCT() function may fail if
+		 * P = Q, in which case we must compute 2Q and use that
+		 * value instead.
+		 */
+		uint z = P.AddCT(Q);
+		Q.DoubleCT();
+		P.Set(Q, ~z);
+
+		/*
+		 * Encode the result. The Encode() function will report
+		 * an error if the addition result is infinity.
+		 */
+		good &= P.Encode(D, compressed);
+		return good;
+	}
+
+	/*
+	 * Generate a new random secret value appropriate for an ECDH
+	 * key exchange (WARNING: this might not be sufficiently uniform
+	 * for the generation of the per-signature secret value 'k' for
+	 * ECDSA).
+	 *
+	 * The value is returned in unsigned big-endian order, in an array
+	 * of the same size of the subgroup order.
+	 */
+	public abstract byte[] MakeRandomSecret();
+
+	/* ============================================================= */
+
+	byte[] subgroupOrder;
+	byte[] cofactor;
+
+	internal ECCurve(byte[] subgroupOrder, byte[] cofactor)
+	{
+		this.subgroupOrder = subgroupOrder;
+		this.cofactor = cofactor;
+	}
+
+	/*
+	 * Create a new mutable point instance, initialized to the point
+	 * at infinity.
+	 *
+	 * (On some curves whose implementations do not support generic
+	 * point addition, this method may return a non-infinity point
+	 * which serves as placeholder to obtain MutableECPoint instances.)
+	 */
+	internal abstract MutableECPoint MakeZero();
+
+	/*
+	 * Create a new mutable point instance, initialized to the
+	 * defined subgroup generator.
+	 */
+	internal abstract MutableECPoint MakeGenerator();
+
+	/*
+	 * Create a new mutable point instance by decoding the provided
+	 * value.
+	 */
+	internal abstract MutableECPoint Decode(byte[] enc);
+}
+
+}
diff --git a/Crypto/ECCurve25519.cs b/Crypto/ECCurve25519.cs
new file mode 100644
index 0000000..c31b813
--- /dev/null
+++ b/Crypto/ECCurve25519.cs
@@ -0,0 +1,248 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * Implementation for Curve25519.
+ */
+
+internal class ECCurve25519 : ECCurve {
+
+	public override string Name {
+		get {
+			return "Curve25519";
+		}
+	}
+
+	public override ECCurveType CurveType {
+		get {
+			return ECCurveType.Montgomery;
+		}
+	}
+
+	public override int EncodedLength {
+		get {
+			return 32;
+		}
+	}
+
+	public override int EncodedLengthCompressed {
+		get {
+			return 32;
+		}
+	}
+
+	public override byte[] GetGenerator(bool compressed)
+	{
+		byte[] G = new byte[32];
+		G[0] = 9;
+		return G;
+	}
+
+	private static byte[] ORDER = {
+		0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+		0x14, 0xDE, 0xF9, 0xDE, 0xA2, 0xF7, 0x9C, 0xD6,
+		0x58, 0x12, 0x63, 0x1A, 0x5C, 0xF5, 0xD3, 0xED
+	};
+
+	private static byte[] COFACTOR = {
+		0x08
+	};
+
+	private static byte[] MOD = {
+		0x7F, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+		0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+		0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
+		0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xED
+	};
+
+	internal ModInt mp;
+	internal ModInt ma24;
+
+	internal ECCurve25519() : base(ORDER, COFACTOR)
+	{
+		mp = new ModInt(MOD);
+		ma24 = mp.Dup();
+		ma24.Set(121665);
+		ma24.ToMonty();
+	}
+
+	public override void CheckValid()
+	{
+		/* Nothing to do, the curve is valid by construction. */
+	}
+
+	public override int GetXoff(out int len)
+	{
+		len = 32;
+		return 0;
+	}
+
+	/* obsolete
+	public override uint Mul(byte[] G, byte[] x, byte[] D, bool compressed)
+	{
+		if (G.Length != 32 || D.Length != 32 || x.Length > 32) {
+			return 0;
+		}
+
+		byte[] k = new byte[32];
+		Array.Copy(x, 0, k, 32 - x.Length, x.Length);
+
+		k[31] &= 0xF8;
+		k[0] &= 0x7F;
+		k[0] |= 0x40;
+
+		byte[] u = new byte[32];
+		for (int i = 0; i < 32; i ++) {
+			u[i] = G[31 - i];
+		}
+		u[0] &= 0x7F;
+
+		ModInt x1 = mp.Dup();
+		x1.DecodeReduce(u);
+		x1.ToMonty();
+		ModInt x2 = mp.Dup();
+		x2.SetMonty(0xFFFFFFFF);
+		ModInt z2 = mp.Dup();
+		ModInt x3 = x1.Dup();
+		ModInt z3 = x2.Dup();
+		uint swap = 0;
+
+		ModInt a = mp.Dup();
+		ModInt aa = mp.Dup();
+		ModInt b = mp.Dup();
+		ModInt bb = mp.Dup();
+		ModInt c = mp.Dup();
+		ModInt d = mp.Dup();
+		ModInt e = mp.Dup();
+
+		for (int t = 254; t >= 0; t --) {
+			uint kt = (uint)-((k[31 - (t >> 3)] >> (t & 7)) & 1);
+			swap ^= kt;
+			x2.CondSwap(x3, swap);
+			z2.CondSwap(z3, swap);
+			swap = kt;
+
+			a.Set(x2);
+			a.Add(z2);
+			aa.Set(a);
+			aa.MontySquare();
+			b.Set(x2);
+			b.Sub(z2);
+			bb.Set(b);
+			bb.MontySquare();
+			e.Set(aa);
+			e.Sub(bb);
+			c.Set(x3);
+			c.Add(z3);
+			d.Set(x3);
+			d.Sub(z3);
+			d.MontyMul(a);
+			c.MontyMul(b);
+			x3.Set(d);
+			x3.Add(c);
+			x3.MontySquare();
+			z3.Set(d);
+			z3.Sub(c);
+			z3.MontySquare();
+			z3.MontyMul(x1);
+			x2.Set(aa);
+			x2.MontyMul(bb);
+			z2.Set(e);
+			z2.MontyMul(ma24);
+			z2.Add(aa);
+			z2.MontyMul(e);
+		}
+		x2.CondSwap(x3, swap);
+		z2.CondSwap(z3, swap);
+
+		z2.FromMonty();
+		z2.Invert();
+		x2.MontyMul(z2);
+
+		x2.Encode(u);
+		for (int i = 0; i < 32; i ++) {
+			D[i] = u[31 - i];
+		}
+		return 0xFFFFFFFF;
+	}
+
+	public override uint MulAdd(byte[] A, byte[] x, byte[] B, byte[] y,
+		byte[] D, bool compressed)
+	{
+		throw new CryptoException(
+			"Operation not supported for Curve25519");
+	}
+	*/
+
+	public override bool Equals(object obj)
+	{
+		return (obj as ECCurve25519) != null;
+	}
+	
+	public override int GetHashCode()
+	{
+		return 0x3E96D5F6;
+	}
+
+	public override byte[] MakeRandomSecret()
+	{
+		/*
+		 * For Curve25519, we simply generate a random 32-byte
+		 * array, to which we apply the "clamping" that will
+		 * be done for point multiplication anyway.
+		 */
+		byte[] x = new byte[32];
+		RNG.GetBytes(x);
+		x[0] &= 0x7F;
+		x[0] |= 0x40;
+		x[31] &= 0xF8;
+		return x;
+	}
+
+	internal override MutableECPoint MakeZero()
+	{
+		return new MutableECPointCurve25519();
+	}
+
+	internal override MutableECPoint MakeGenerator()
+	{
+		MutableECPointCurve25519 G = new MutableECPointCurve25519();
+		G.Decode(GetGenerator(false));
+		return G;
+	}
+
+	internal override MutableECPoint Decode(byte[] enc)
+	{
+		MutableECPointCurve25519 P = new MutableECPointCurve25519();
+		P.Decode(enc);
+		return P;
+	}
+}
+
+}
diff --git a/Crypto/ECCurvePrime.cs b/Crypto/ECCurvePrime.cs
new file mode 100644
index 0000000..f2621bb
--- /dev/null
+++ b/Crypto/ECCurvePrime.cs
@@ -0,0 +1,337 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * Implementation for elliptic curves in a prime field.
+ */
+
+internal class ECCurvePrime : ECCurve {
+
+	public override string Name {
+		get {
+			return name;
+		}
+	}
+
+	public override ECCurveType CurveType {
+		get {
+			return ECCurveType.Prime;
+		}
+	}
+
+	public override int EncodedLength {
+		get {
+			return 1 + (flen << 1);
+		}
+	}
+
+	public override int EncodedLengthCompressed {
+		get {
+			return 1 + flen;
+		}
+	}
+
+	string name;
+
+	/*
+	 * 'mp' always contains 0 modulo p.
+	 * 'ma' contains a.
+	 * 'mb' contains b.
+	 * pMod4 is p modulo 4.
+	 */
+	internal ModInt mp;
+	internal ModInt ma;
+	internal ModInt mb;
+	internal int pMod4;
+
+	/*
+	 * a and b are in unsigned big-endian notation.
+	 * aIsM3 is true when a == -3 modulo p.
+	 */
+	internal byte[] a;
+	internal byte[] b;
+	internal bool aIsM3;
+
+	internal byte[] mod;
+	internal int flen;
+	byte[] gx;
+	byte[] gy;
+	int hashCode;
+
+	/*
+	 * Checks enforced by the constructor:
+	 * -- modulus is odd and at least 80-bit long
+	 * -- subgroup order is odd and at least 30-bit long
+	 * -- parameters a[] and b[] are lower than modulus
+	 * -- coordinates gx and gy are lower than modulus
+	 * -- coordinates gx and gy match curve equation
+	 */
+	internal ECCurvePrime(string name, byte[] mod, byte[] a, byte[] b,
+		byte[] gx, byte[] gy, byte[] subgroupOrder, byte[] cofactor)
+		: base(subgroupOrder, cofactor)
+	{
+		this.mod = mod = BigInt.NormalizeBE(mod);
+		int modLen = BigInt.BitLength(mod);
+		if (modLen < 80) {
+			throw new CryptoException(
+				"Invalid curve: modulus is too small");
+		}
+		if ((mod[mod.Length - 1] & 0x01) == 0) {
+			throw new CryptoException(
+				"Invalid curve: modulus is even");
+		}
+		int sgLen = BigInt.BitLength(subgroupOrder);
+		if (sgLen < 30) {
+			throw new CryptoException(
+				"Invalid curve: subgroup is too small");
+		}
+		if ((subgroupOrder[subgroupOrder.Length - 1] & 0x01) == 0) {
+			throw new CryptoException(
+				"Invalid curve: subgroup order is even");
+		}
+
+		mp = new ModInt(mod);
+		flen = (modLen + 7) >> 3;
+		pMod4 = mod[mod.Length - 1] & 3;
+
+		this.a = a = BigInt.NormalizeBE(a);
+		this.b = b = BigInt.NormalizeBE(b);
+		if (BigInt.CompareCT(a, mod) >= 0
+			|| BigInt.CompareCT(b, mod) >= 0)
+		{
+			throw new CryptoException(
+				"Invalid curve: out-of-range parameter");
+		}
+		ma = mp.Dup();
+		ma.Decode(a);
+		ma.Add(3);
+		aIsM3 = ma.IsZero;
+		ma.Sub(3);
+		mb = mp.Dup();
+		mb.Decode(b);
+
+		this.gx = gx = BigInt.NormalizeBE(gx);
+		this.gy = gy = BigInt.NormalizeBE(gy);
+		if (BigInt.CompareCT(gx, mod) >= 0
+			|| BigInt.CompareCT(gy, mod) >= 0)
+		{
+			throw new CryptoException(
+				"Invalid curve: out-of-range coordinates");
+		}
+		MutableECPointPrime G = new MutableECPointPrime(this);
+		G.Set(gx, gy, true);
+
+		hashCode = (int)(BigInt.HashInt(mod)
+			^ BigInt.HashInt(a) ^ BigInt.HashInt(b)
+			^ BigInt.HashInt(gx) ^ BigInt.HashInt(gy));
+
+		if (name == null) {
+			name = string.Format("generic prime {0}/{1}",
+				modLen, sgLen);
+		}
+		this.name = name;
+	}
+
+	/*
+	 * Extra checks:
+	 * -- modulus is prime
+	 * -- subgroup order is prime
+	 * -- generator indeed generates subgroup
+	 */
+	public override void CheckValid()
+	{
+		/*
+		 * Check that the modulus is prime.
+		 */
+		if (!BigInt.IsPrime(mod)) {
+			throw new CryptoException(
+				"Invalid curve: modulus is not prime");
+		}
+
+		/*
+		 * Check that the subgroup order is prime.
+		 */
+		if (!BigInt.IsPrime(SubgroupOrder)) {
+			throw new CryptoException(
+				"Invalid curve: subgroup order is not prime");
+		}
+
+		/*
+		 * Check that the G point is indeed a generator of the
+		 * subgroup. Note that since it has explicit coordinates,
+		 * it cannot be the point at infinity; it suffices to
+		 * verify that, when multiplied by the subgroup order,
+		 * it yields infinity.
+		 */
+		MutableECPointPrime G = new MutableECPointPrime(this);
+		G.Set(gx, gy, false);
+		if (G.MulSpecCT(SubgroupOrder) == 0 || !G.IsInfinity) {
+			throw new CryptoException(
+				"Invalid curve: generator does not match"
+				+ " subgroup order");
+		}
+
+		/*
+		 * TODO: check cofactor.
+		 *
+		 * If the cofactor is small, then we can simply compute
+		 * the complete curve order by multiplying the cofactor
+		 * with the subgroup order, and see whether it is in the
+		 * proper range with regards to the field cardinal (by
+		 * using Hasse's theorem). However, if the cofactor is
+		 * larger than the subgroup order, then detecting a
+		 * wrong cofactor value is a bit more complex. We could
+		 * generate a few random points and multiply them by
+		 * the computed order, but this may be expensive.
+		 */
+	}
+
+	public override int GetXoff(out int len)
+	{
+		len = flen;
+		return 1;
+	}
+
+	/* obsolete
+	public override uint Mul(byte[] G, byte[] x, byte[] D, bool compressed)
+	{
+		MutableECPointPrime P = new MutableECPointPrime(this);
+		uint good = P.DecodeCT(G);
+		good &= ~P.IsInfinityCT;
+		good &= P.MulSpecCT(x);
+		good &= P.Encode(D, compressed);
+		return good;
+	}
+
+	public override uint MulAdd(byte[] A, byte[] x, byte[] B, byte[] y,
+		byte[] D, bool compressed)
+	{
+		MutableECPointPrime P = new MutableECPointPrime(this);
+		MutableECPointPrime Q = new MutableECPointPrime(this);
+
+		uint good = P.DecodeCT(A);
+		good &= Q.DecodeCT(B);
+		good &= ~P.IsInfinityCT & ~Q.IsInfinityCT;
+
+		good &= P.MulSpecCT(x);
+		good &= Q.MulSpecCT(y);
+		good &= ~P.IsInfinityCT & ~Q.IsInfinityCT;
+
+		uint z = P.AddCT(Q);
+		Q.DoubleCT();
+		P.Set(Q, ~z);
+
+		good &= P.Encode(D, compressed);
+		return good;
+	}
+	*/
+
+	public override byte[] MakeRandomSecret()
+	{
+		/*
+		 * We force the top bits to 0 to guarantee that the value
+		 * is less than the subgroup order; and we force the
+		 * least significant bit to 0 so that the value is not null.
+		 * This is good enough for ECDH.
+		 */
+		byte[] q = SubgroupOrder;
+		byte[] x = new byte[q.Length];
+		int mask = 0xFF;
+		while (mask >= q[0]) {
+			mask >>= 1;
+		}
+		RNG.GetBytes(x);
+		x[0] &= (byte)mask;
+		x[x.Length - 1] |= (byte)0x01;
+		return x;
+	}
+
+	internal override MutableECPoint MakeZero()
+	{
+		return new MutableECPointPrime(this);
+	}
+
+	internal override MutableECPoint MakeGenerator()
+	{
+		/*
+		 * We do not have to check the generator, since
+		 * it was already done in the constructor.
+		 */
+		MutableECPointPrime G = new MutableECPointPrime(this);
+		G.Set(gx, gy, false);
+		return G;
+	}
+
+	internal override MutableECPoint Decode(byte[] enc)
+	{
+		MutableECPointPrime P = new MutableECPointPrime(this);
+		P.Decode(enc);
+		return P;
+	}
+	
+	public override bool Equals(object obj)
+	{
+		return Equals(obj as ECCurvePrime);
+	}
+	
+	internal bool Equals(ECCurvePrime curve)
+	{
+		if (this == curve) {
+			return true;
+		}
+		return BigInt.Compare(mod, curve.mod) == 0
+			&& BigInt.Compare(a, curve.a) == 0
+			&& BigInt.Compare(b, curve.b) == 0
+			&& BigInt.Compare(gx, curve.gx) == 0
+			&& BigInt.Compare(gy, curve.gy) == 0;
+	}
+
+	public override int GetHashCode()
+	{
+		return hashCode;
+	}
+
+	/*
+	 * Given a value X in sx, this method computes X^3+aX+b into sd.
+	 * 'sx' is unmodified. 'st' is modified (it receives a*X).
+	 * The sx, sd and st instances MUST be distinct.
+	 */
+	internal void RebuildY2(ModInt sx, ModInt sd, ModInt st)
+	{
+		sd.Set(sx);
+		sd.ToMonty();
+		st.Set(sd);
+		sd.MontySquare();
+		sd.MontyMul(sx);
+		st.MontyMul(ma);
+		sd.Add(st);
+		sd.Add(mb);
+	}
+}
+
+}
diff --git a/Crypto/ECCurveType.cs b/Crypto/ECCurveType.cs
new file mode 100644
index 0000000..c875c0a
--- /dev/null
+++ b/Crypto/ECCurveType.cs
@@ -0,0 +1,42 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * Symbolic identifiers for curve types.
+ */
+
+public enum ECCurveType {
+
+	Prime,
+	BinaryTrinomial,
+	BinaryPentanomial,
+	Montgomery,
+	Edwards
+}
+
+}
diff --git a/Crypto/ECDSA.cs b/Crypto/ECDSA.cs
new file mode 100644
index 0000000..e10e6d6
--- /dev/null
+++ b/Crypto/ECDSA.cs
@@ -0,0 +1,400 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.IO;
+
+namespace Crypto {
+
+/*
+ * This class implements the ECDSA signature algorithm. The static methods
+ * provide access to the algorithm primitives. The hash of the signed data
+ * must be provided externally.
+ *
+ * Signatures may be encoded in either "ASN.1" or "raw" formats; "ASN.1"
+ * is normally used (e.g. in SSL/TLS) but some protocols and API expect
+ * the raw format (e.g. PKCS#11 and OpenPGP). An ECDSA signature consists
+ * in two integers r and s, which are values modulo the subgroup order q.
+ * In ASN.1 format, the signature is the DER encoding of the ASN.1
+ * structure:
+ *
+ *    ECDSA-signature ::= SEQUENCE {
+ *            r INTEGER,
+ *            s INTEGER
+ *    }
+ *
+ * In raw format, the two integers r and s are encoded with unsigned
+ * big-endian encoding (with the same encoding length) and concatenated.
+ *
+ * The Sign() and Verify() methods use the ASN.1 format. The SignRaw()
+ * and VerifyRaw() use the raw format. The SigRawToAsn1() and SigAsn1ToRaw()
+ * allow for converting between the two formats.
+ */
+
+public class ECDSA : DSAUtils {
+
+	/*
+	 * Verify an ECDSA signature (ASN.1 format). Returned value is true
+	 * on success. If the signature is invalid, then false is returned.
+	 * Internal exceptions (due to an incorrect public key) are also
+	 * converted to a returned value of false.
+	 *
+	 * There are four methods, depending on the source operands.
+	 */
+
+	public static bool Verify(ECPublicKey pk,
+		byte[] hash, byte[] sig)
+	{
+		return Verify(pk, hash, 0, hash.Length, sig, 0, sig.Length);
+	}
+
+	public static bool Verify(ECPublicKey pk,
+		byte[] hash, int hashOff, int hashLen, byte[] sig)
+	{
+		return Verify(pk, hash, hashOff, hashLen, sig, 0, sig.Length);
+	}
+
+	public static bool Verify(ECPublicKey pk,
+		byte[] hash, byte[] sig, int sigOff, int sigLen)
+	{
+		return Verify(pk, hash, 0, hash.Length, sig, sigOff, sigLen);
+	}
+
+	public static bool Verify(ECPublicKey pk,
+		byte[] hash, int hashOff, int hashLen,
+		byte[] sig, int sigOff, int sigLen)
+	{
+		byte[] raw = SigAsn1ToRaw(sig, sigOff, sigLen);
+		if (raw == null) {
+			return false;
+		}
+		return VerifyRaw(pk,
+			hash, hashOff, hashLen, raw, 0, raw.Length);
+	}
+
+	/*
+	 * Verify an ECDSA signature (raw format). Returned value is true
+	 * on success. If the signature is invalid, then false is returned.
+	 * Internal exceptions (due to an incorrect public key) are also
+	 * converted to a returned value of false.
+	 *
+	 * There are four methods, depending on the source operands.
+	 */
+
+	public static bool VerifyRaw(ECPublicKey pk,
+		byte[] hash, byte[] sig)
+	{
+		return VerifyRaw(pk,
+			hash, 0, hash.Length, sig, 0, sig.Length);
+	}
+
+	public static bool VerifyRaw(ECPublicKey pk,
+		byte[] hash, int hashOff, int hashLen, byte[] sig)
+	{
+		return VerifyRaw(pk,
+			hash, hashOff, hashLen, sig, 0, sig.Length);
+	}
+
+	public static bool VerifyRaw(ECPublicKey pk,
+		byte[] hash, byte[] sig, int sigOff, int sigLen)
+	{
+		return VerifyRaw(pk,
+			hash, 0, hash.Length, sig, sigOff, sigLen);
+	}
+
+	public static bool VerifyRaw(ECPublicKey pk,
+		byte[] hash, int hashOff, int hashLen,
+		byte[] sig, int sigOff, int sigLen)
+	{
+		try {
+			/*
+			 * Get the curve.
+			 */
+			ECCurve curve = pk.Curve;
+
+			/*
+			 * Get r and s from signature. This also verifies
+			 * that they do not exceed the subgroup order.
+			 */
+			if (sigLen == 0 || (sigLen & 1) != 0) {
+				return false;
+			}
+			int tlen = sigLen >> 1;
+			ModInt oneQ = new ModInt(curve.SubgroupOrder);
+			oneQ.Set(1);
+			ModInt r = oneQ.Dup();
+			ModInt s = oneQ.Dup();
+			r.Decode(sig, sigOff, tlen);
+			s.Decode(sig, sigOff + tlen, tlen);
+
+			/*
+			 * If either r or s was too large, it got set to
+			 * zero. We also don't want real zeros.
+			 */
+			if (r.IsZero || s.IsZero) {
+				return false;
+			}
+
+			/*
+			 * Convert the hash value to an integer modulo q.
+			 * As per FIPS 186-4, if the hash value is larger
+			 * than q, then we keep the qlen leftmost bits of
+			 * the hash value.
+			 */
+			int qBitLength = oneQ.ModBitLength;
+			int hBitLength = hashLen << 3;
+			byte[] hv;
+			if (hBitLength <= qBitLength) {
+				hv = new byte[hashLen];
+				Array.Copy(hash, hashOff, hv, 0, hashLen);
+			} else {
+				int qlen = (qBitLength + 7) >> 3;
+				hv = new byte[qlen];
+				Array.Copy(hash, hashOff, hv, 0, qlen);
+				int rs = (8 - (qBitLength & 7)) & 7;
+				BigInt.RShift(hv, rs);
+			}
+			ModInt z = oneQ.Dup();
+			z.DecodeReduce(hv);
+
+			/*
+			 * Apply the verification algorithm:
+			 *   w = 1/s mod q
+			 *   u = z*w mod q
+			 *   v = r*w mod q
+			 *   T = u*G + v*Pub
+			 *   test whether T.x mod q == r.
+			 */
+			/*
+			 * w = 1/s mod q
+			 */
+			ModInt w = s.Dup();
+			w.Invert();
+
+			/*
+			 * u = z*w mod q
+			 */
+			w.ToMonty();
+			ModInt u = w.Dup();
+			u.MontyMul(z);
+
+			/*
+			 * v = r*w mod q
+			 */
+			ModInt v = w.Dup();
+			v.MontyMul(r);
+
+			/*
+			 * Compute u*G
+			 */
+			MutableECPoint T = curve.MakeGenerator();
+			uint good = T.MulSpecCT(u.Encode());
+
+			/*
+			 * Compute v*iPub
+			 */
+			MutableECPoint M = pk.iPub.Dup();
+			good &= M.MulSpecCT(v.Encode());
+
+			/*
+			 * Compute T = u*G+v*iPub
+			 */
+			uint nd = T.AddCT(M);
+			M.DoubleCT();
+			T.Set(M, ~nd);
+			good &= ~T.IsInfinityCT;
+
+			/*
+			 * Get T.x, reduced modulo q.
+			 * Signature is valid if and only if we get
+			 * the same value as r (and we did not encounter
+			 * an error previously).
+			 */
+			s.DecodeReduce(T.X);
+			return (good & r.EqCT(s)) != 0;
+
+		} catch (CryptoException) {
+			/*
+			 * Exceptions may occur if the key or signature
+			 * have invalid values (non invertible, out of
+			 * range...). Any such occurrence means that the
+			 * signature is not valid.
+			 */
+			return false;
+		}
+	}
+
+	/*
+	 * Compute an ECDSA signature (ASN.1 format). On error (e.g. due
+	 * to an invalid private key), an exception is thrown.
+	 *
+	 * Internally, the process described in RFC 6979 is used to
+	 * compute the per-signature random value 'k'. If 'rfc6979Hash'
+	 * is not null, then a clone of that function is used for that
+	 * process, and signatures are fully deterministic and should
+	 * match RFC 6979 test vectors; if 'rfc6979Hash' is null, then
+	 * the engine uses SHA-256 with additional randomness, resulting
+	 * in randomized signatures. The systematic use of RFC 6979 in
+	 * both cases ensures the safety of the private key even if the
+	 * system RNG is predictible.
+	 *
+	 * There are four methods, depending on the source operands.
+	 */
+
+	public static byte[] Sign(ECPrivateKey sk, IDigest rfc6979Hash,
+		byte[] hash)
+	{
+		return Sign(sk, rfc6979Hash, hash, 0, hash.Length);
+	}
+
+	public static byte[] Sign(ECPrivateKey sk, IDigest rfc6979Hash,
+		byte[] hash, int hashOff, int hashLen)
+	{
+		return SigRawToAsn1(SignRaw(sk, rfc6979Hash,
+			hash, hashOff, hashLen));
+	}
+
+	public static int Sign(ECPrivateKey sk, IDigest rfc6979Hash,
+		byte[] hash, byte[] outBuf, int outOff)
+	{
+		return Sign(sk, rfc6979Hash,
+			hash, 0, hash.Length, outBuf, outOff);
+	}
+
+	public static int Sign(ECPrivateKey sk, IDigest rfc6979Hash,
+		byte[] hash, int hashOff, int hashLen,
+		byte[] outBuf, int outOff)
+	{
+		byte[] sig = Sign(sk, rfc6979Hash, hash, hashOff, hashLen);
+		Array.Copy(sig, 0, outBuf, outOff, sig.Length);
+		return sig.Length;
+	}
+
+	/*
+	 * Compute an ECDSA signature (raw format). On error (e.g. due
+	 * to an invalid private key), an exception is thrown.
+	 *
+	 * Internally, the process described in RFC 6979 is used to
+	 * compute the per-signature random value 'k'. If 'rfc6979Hash'
+	 * is not null, then a clone of that function is used for that
+	 * process, and signatures are fully deterministic and should
+	 * match RFC 6979 test vectors; if 'rfc6979Hash' is null, then
+	 * the engine uses SHA-256 with additional randomness, resulting
+	 * in randomized signatures. The systematic use of RFC 6979 in
+	 * both cases ensures the safety of the private key even if the
+	 * system RNG is predictible.
+	 *
+	 * The signature returned by these methods always has length
+	 * exactly twice that of the encoded subgroup order (they are
+	 * not minimalized). Use SigRawMinimalize() to reduce the
+	 * signature size to its minimum length.
+	 *
+	 * There are four methods, depending on the source operands.
+	 */
+
+	public static byte[] SignRaw(ECPrivateKey sk, IDigest rfc6979Hash,
+		byte[] hash)
+	{
+		return SignRaw(sk, rfc6979Hash, hash, 0, hash.Length);
+	}
+
+	public static int SignRaw(ECPrivateKey sk, IDigest rfc6979Hash,
+		byte[] hash, byte[] outBuf, int outOff)
+	{
+		return SignRaw(sk, rfc6979Hash,
+			hash, 0, hash.Length, outBuf, outOff);
+	}
+
+	public static int SignRaw(ECPrivateKey sk, IDigest rfc6979Hash,
+		byte[] hash, int hashOff, int hashLen,
+		byte[] outBuf, int outOff)
+	{
+		byte[] sig = SignRaw(sk, rfc6979Hash, hash, hashOff, hashLen);
+		Array.Copy(sig, 0, outBuf, outOff, sig.Length);
+		return sig.Length;
+	}
+
+	public static byte[] SignRaw(ECPrivateKey sk, IDigest rfc6979Hash,
+		byte[] hash, int hashOff, int hashLen)
+	{
+		ECCurve curve = sk.Curve;
+		byte[] q = curve.SubgroupOrder;
+		RFC6979 rf = new RFC6979(rfc6979Hash, q, sk.X,
+			hash, hashOff, hashLen, rfc6979Hash != null);
+		ModInt mh = rf.GetHashMod();
+		ModInt mx = mh.Dup();
+		mx.Decode(sk.X);
+
+		/*
+		 * Compute DSA signature. We use a loop to enumerate
+		 * candidates for k until a proper one is found (it
+		 * is VERY improbable that we may have to loop).
+		 */
+		ModInt mr = mh.Dup();
+		ModInt ms = mh.Dup();
+		ModInt mk = mh.Dup();
+		byte[] k = new byte[q.Length];
+		for (;;) {
+			rf.NextK(k);
+			MutableECPoint G = curve.MakeGenerator();
+			if (G.MulSpecCT(k) == 0) {
+				/*
+				 * We may get an error here only if the
+				 * curve is invalid (generator does not
+				 * produce the expected subgroup).
+				 */
+				throw new CryptoException(
+					"Invalid EC private key / curve");
+			}
+			mr.DecodeReduce(G.X);
+			if (mr.IsZero) {
+				continue;
+			}
+			ms.Set(mx);
+			ms.ToMonty();
+			ms.MontyMul(mr);
+			ms.Add(mh);
+			mk.Decode(k);
+			mk.Invert();
+			ms.ToMonty();
+			ms.MontyMul(mk);
+
+			byte[] sig = new byte[q.Length << 1];
+			mr.Encode(sig, 0, q.Length);
+			ms.Encode(sig, q.Length, q.Length);
+			return sig;
+		}
+	}
+
+	/*
+	 * Generate a new EC key pair in the specified curve.
+	 */
+	public static ECPrivateKey Generate(ECCurve curve)
+	{
+		return new ECPrivateKey(curve,
+			BigInt.RandIntNZ(curve.SubgroupOrder));
+	}
+}
+
+}
diff --git a/Crypto/ECPrivateKey.cs b/Crypto/ECPrivateKey.cs
new file mode 100644
index 0000000..d63692d
--- /dev/null
+++ b/Crypto/ECPrivateKey.cs
@@ -0,0 +1,113 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * This class contains an EC private key, consisting of two elements:
+ * -- an elliptic curve
+ * -- the private integer (X)
+ *
+ * The private integer is always handled as an integer modulo the
+ * curve subbroup order. Its binary representation is unsigned big-endian
+ * with exactly the same length as the subgroup order.
+ */
+
+public class ECPrivateKey : IPrivateKey {
+
+	public ECCurve Curve {
+		get {
+			return curve;
+		}
+	}
+
+	public byte[] X {
+		get {
+			return priv;
+		}
+	}
+
+	public int KeySizeBits {
+		get {
+			return BigInt.BitLength(curve.SubgroupOrder);
+		}
+	}
+
+	public string AlgorithmName {
+		get {
+			return "EC";
+		}
+	}
+
+	IPublicKey IPrivateKey.PublicKey {
+		get {
+			return this.PublicKey;
+		}
+	}
+
+	public ECPublicKey PublicKey {
+		get {
+			if (dpk == null) {
+				MutableECPoint G = curve.MakeGenerator();
+				G.MulSpecCT(priv);
+				dpk = new ECPublicKey(curve, G.Encode(false));
+			}
+			return dpk;
+		}
+	}
+
+	ECCurve curve;
+	byte[] priv;
+	ECPublicKey dpk;
+
+	/*
+	 * Create a new instance with the provided elements. The
+	 * constructor verifies that the provided private integer
+	 * is non-zero and is less than the subgroup order.
+	 */
+	public ECPrivateKey(ECCurve curve, byte[] X)
+	{
+		this.curve = curve;
+		ModInt ms = new ModInt(curve.SubgroupOrder);
+		uint good = ms.Decode(X);
+		good &= ~ms.IsZeroCT;
+		if (good == 0) {
+			throw new CryptoException("Invalid private key");
+		}
+		priv = ms.Encode();
+		dpk = null;
+	}
+
+	/*
+	 * CheckValid() runs the validity tests on the curve.
+	 */
+	public void CheckValid()
+	{
+		curve.CheckValid();
+	}
+}
+
+}
diff --git a/Crypto/ECPublicKey.cs b/Crypto/ECPublicKey.cs
new file mode 100644
index 0000000..9db1c86
--- /dev/null
+++ b/Crypto/ECPublicKey.cs
@@ -0,0 +1,121 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * This class contains an EC public key, consisting of two elements:
+ * -- an elliptic curve
+ * -- a non-zero point on that curve (called "Pub")
+ */
+
+public class ECPublicKey : IPublicKey {
+
+	public ECCurve Curve {
+		get {
+			return curve;
+		}
+	}
+
+	public byte[] Pub {
+		get {
+			return pub;
+		}
+	}
+
+	public int KeySizeBits {
+		get {
+			return BigInt.BitLength(curve.SubgroupOrder);
+		}
+	}
+
+	public string AlgorithmName {
+		get {
+			return "EC";
+		}
+	}
+
+	ECCurve curve;
+	byte[] pub;
+	int hashCode;
+
+	internal MutableECPoint iPub;
+
+	/*
+	 * Create a new instance with the provided elements. The
+	 * constructor verifies that the provided point is part of
+	 * the curve.
+	 */
+	public ECPublicKey(ECCurve curve, byte[] Pub)
+	{
+		this.curve = curve;
+		this.pub = Pub;
+		iPub = curve.Decode(Pub);
+		if (iPub.IsInfinity) {
+			throw new CryptoException(
+				"Public key point is infinity");
+		}
+		hashCode = curve.GetHashCode()
+			^ (int)BigInt.HashInt(iPub.X)
+			^ (int)BigInt.HashInt(iPub.Y);
+	}
+
+	/*
+	 * CheckValid() runs the validity tests on the curve, and
+	 * verifies that provided point is part of a subgroup with
+	 * the advertised subgroup order.
+	 */
+	public void CheckValid()
+	{
+		curve.CheckValid();
+		MutableECPoint P = iPub.Dup();
+		if (P.MulSpecCT(curve.SubgroupOrder) == 0
+			|| !P.IsInfinity)
+		{
+			throw new CryptoException(
+				"Public key point not on the defined subgroup");
+		}
+	}
+
+	public override bool Equals(object obj)
+	{
+		ECPublicKey pk = obj as ECPublicKey;
+		if (pk == null) {
+			return false;
+		}
+		if (hashCode != pk.hashCode) {
+			return false;
+		}
+		return iPub.Eq(pk.iPub);
+	}
+
+	public override int GetHashCode()
+	{
+		return hashCode;
+	}
+}
+
+}
diff --git a/Crypto/GHASH.cs b/Crypto/GHASH.cs
new file mode 100644
index 0000000..7a5fe3c
--- /dev/null
+++ b/Crypto/GHASH.cs
@@ -0,0 +1,237 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * GHASH implementation using integer multiplications (32->64). It is
+ * constant-time, provided that the platform multiplication opcode is
+ * constant-time.
+ */
+
+public sealed class GHASH {
+
+	/*
+	 * Compute GHASH over the provided data. The y[] array is
+	 * updated, using the h[] secret value. If the data length
+	 * is not a multiple of 16 bytes, then it is right-padded
+	 * with zeros.
+	 */
+	public static void Run(byte[] y, byte[] h, byte[] data)
+	{
+		Run(y, h, data, 0, data.Length);
+	}
+
+	/*
+	 * Compute GHASH over the provided data. The y[] array is
+	 * updated, using the h[] secret value. If the data length
+	 * is not a multiple of 16 bytes, then it is right-padded
+	 * with zeros.
+	 */
+	public static void Run(byte[] y, byte[] h,
+		byte[] data, int off, int len)
+	{
+		uint y0, y1, y2, y3;
+		uint h0, h1, h2, h3;
+		y3 = Dec32be(y,  0);
+		y2 = Dec32be(y,  4);
+		y1 = Dec32be(y,  8);
+		y0 = Dec32be(y, 12);
+		h3 = Dec32be(h,  0);
+		h2 = Dec32be(h,  4);
+		h1 = Dec32be(h,  8);
+		h0 = Dec32be(h, 12);
+
+		while (len > 0) {
+			/*
+			 * Decode the next block and add it (XOR) into the
+			 * current state.
+			 */
+			if (len >= 16) {
+				y3 ^= Dec32be(data, off);
+				y2 ^= Dec32be(data, off + 4);
+				y1 ^= Dec32be(data, off + 8);
+				y0 ^= Dec32be(data, off + 12);
+			} else {
+				y3 ^= Dec32bePartial(data, off +  0, len -  0);
+				y2 ^= Dec32bePartial(data, off +  4, len -  4);
+				y1 ^= Dec32bePartial(data, off +  8, len -  8);
+				y0 ^= Dec32bePartial(data, off + 12, len - 12);
+			}
+			off += 16;
+			len -= 16;
+
+			/*
+			 * We multiply two 128-bit field elements with
+			 * two Karatsuba levels, to get down to nine
+			 * 32->64 multiplications.
+			 */
+			uint a0 = y0;
+			uint b0 = h0;
+			uint a1 = y1;
+			uint b1 = h1;
+			uint a2 = a0 ^ a1;
+			uint b2 = b0 ^ b1;
+
+			uint a3 = y2;
+			uint b3 = h2;
+			uint a4 = y3;
+			uint b4 = h3;
+			uint a5 = a3 ^ a4;
+			uint b5 = b3 ^ b4;
+
+			uint a6 = a0 ^ a3;
+			uint b6 = b0 ^ b3;
+			uint a7 = a1 ^ a4;
+			uint b7 = b1 ^ b4;
+			uint a8 = a6 ^ a7;
+			uint b8 = b6 ^ b7;
+
+			ulong z0 = BMul(a0, b0);
+			ulong z1 = BMul(a1, b1);
+			ulong z2 = BMul(a2, b2);
+			ulong z3 = BMul(a3, b3);
+			ulong z4 = BMul(a4, b4);
+			ulong z5 = BMul(a5, b5);
+			ulong z6 = BMul(a6, b6);
+			ulong z7 = BMul(a7, b7);
+			ulong z8 = BMul(a8, b8);
+
+			z2 ^= z0 ^ z1;
+			z0 ^= z2 << 32;
+			z1 ^= z2 >> 32;
+
+			z5 ^= z3 ^ z4;
+			z3 ^= z5 << 32;
+			z4 ^= z5 >> 32;
+
+			z8 ^= z6 ^ z7;
+			z6 ^= z8 << 32;
+			z7 ^= z8 >> 32;
+
+			z6 ^= z0 ^ z3;
+			z7 ^= z1 ^ z4;
+			z1 ^= z6;
+			z3 ^= z7;
+
+			/*
+			 * 255-bit product is now in z4:z3:z1:z0. Since
+			 * the GHASH specification uses a "reversed"
+			 * notation, our 255-bit result must be shifted
+			 * by 1 bit to the left.
+			 */
+			z4 = (z4 << 1) | (z3 >> 63);
+			z3 = (z3 << 1) | (z1 >> 63);
+			z1 = (z1 << 1) | (z0 >> 63);
+			z0 = (z0 << 1);
+
+			/*
+			 * Apply reduction modulo the degree-128
+			 * polynomial that defines the field.
+			 */
+			z3 ^= z0 ^ (z0 >> 1) ^ (z0 >> 2) ^ (z0 >> 7);
+			z1 ^= (z0 << 63) ^ (z0 << 62) ^ (z0 << 57);
+			z4 ^= z1 ^ (z1 >> 1) ^ (z1 >> 2) ^ (z1 >> 7);
+			z3 ^= (z1 << 63) ^ (z1 << 62) ^ (z1 << 57);
+
+			/*
+			 * The reduced result is the new "y" state.
+			 */
+			y0 = (uint)z3;
+			y1 = (uint)(z3 >> 32);
+			y2 = (uint)z4;
+			y3 = (uint)(z4 >> 32);
+		}
+
+		Enc32be(y3, y,  0);
+		Enc32be(y2, y,  4);
+		Enc32be(y1, y,  8);
+		Enc32be(y0, y, 12);
+	}
+
+	static ulong BMul(uint x, uint y)
+	{
+		ulong x0, x1, x2, x3;
+		ulong y0, y1, y2, y3;
+		ulong z0, z1, z2, z3;
+		x0 = (ulong)(x & 0x11111111);
+		x1 = (ulong)(x & 0x22222222);
+		x2 = (ulong)(x & 0x44444444);
+		x3 = (ulong)(x & 0x88888888);
+		y0 = (ulong)(y & 0x11111111);
+		y1 = (ulong)(y & 0x22222222);
+		y2 = (ulong)(y & 0x44444444);
+		y3 = (ulong)(y & 0x88888888);
+		z0 = (x0 * y0) ^ (x1 * y3) ^ (x2 * y2) ^ (x3 * y1);
+		z1 = (x0 * y1) ^ (x1 * y0) ^ (x2 * y3) ^ (x3 * y2);
+		z2 = (x0 * y2) ^ (x1 * y1) ^ (x2 * y0) ^ (x3 * y3);
+		z3 = (x0 * y3) ^ (x1 * y2) ^ (x2 * y1) ^ (x3 * y0);
+		z0 &= 0x1111111111111111;
+		z1 &= 0x2222222222222222;
+		z2 &= 0x4444444444444444;
+		z3 &= 0x8888888888888888;
+		return z0 | z1 | z2 | z3;
+	}
+
+	static uint Dec32be(byte[] buf, int off)
+	{
+		return ((uint)buf[off + 0] << 24)
+			| ((uint)buf[off + 1] << 16)
+			| ((uint)buf[off + 2] << 8)
+			| (uint)buf[off + 3];
+	}
+
+	static void Enc32be(uint x, byte[] buf, int off)
+	{
+		buf[off + 0] = (byte)(x >> 24);
+		buf[off + 1] = (byte)(x >> 16);
+		buf[off + 2] = (byte)(x >> 8);
+		buf[off + 3] = (byte)x;
+	}
+
+	static uint Dec32bePartial(byte[] buf, int off, int len)
+	{
+		if (len >= 4) {
+			return ((uint)buf[off + 0] << 24)
+				| ((uint)buf[off + 1] << 16)
+				| ((uint)buf[off + 2] << 8)
+				| (uint)buf[off + 3];
+		} else if (len >= 3) {
+			return ((uint)buf[off + 0] << 24)
+				| ((uint)buf[off + 1] << 16)
+				| ((uint)buf[off + 2] << 8);
+		} else if (len >= 2) {
+			return ((uint)buf[off + 0] << 24)
+				| ((uint)buf[off + 1] << 16);
+		} else if (len >= 1) {
+			return ((uint)buf[off + 0] << 24);
+		} else {
+			return 0;
+		}
+	}
+}
+
+}
diff --git a/Crypto/HMAC.cs b/Crypto/HMAC.cs
new file mode 100644
index 0000000..fcb1e29
--- /dev/null
+++ b/Crypto/HMAC.cs
@@ -0,0 +1,366 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * Implementation of HMAC (RFC 2104).
+ */
+
+public sealed class HMAC {
+
+	IDigest h;
+	byte[] key;
+	int keyLen;
+	byte[] tmp, tmpCT;
+	ulong dataLen;
+
+	/*
+	 * Create a new instance, using the provided hash function. The
+	 * hash function instance will be used internally.
+	 */
+	public HMAC(IDigest h) : this(h, true)
+	{
+	}
+
+	private HMAC(IDigest h, bool doReset)
+	{
+		this.h = h;
+		int n = h.BlockSize;
+		if (n < h.DigestSize) {
+			throw new ArgumentException(
+				"invalid hash function for HMAC");
+		}
+		key = new byte[n];
+		keyLen = 0;
+		tmp = new byte[h.DigestSize];
+		tmpCT = new byte[h.DigestSize];
+		if (doReset) {
+			Reset();
+		}
+	}
+
+	/*
+	 * Duplicate this engine. The returned instance inherits the
+	 * current state (key, data already processed...) but is
+	 * thereafter independent.
+	 */
+	public HMAC Dup()
+	{
+		HMAC hm = new HMAC(h.Dup(), false);
+		Array.Copy(key, 0, hm.key, 0, keyLen);
+		hm.keyLen = keyLen;
+		hm.dataLen = dataLen;
+		return hm;
+	}
+
+	/*
+	 * Get the HMAC output size (in bytes).
+	 */
+	public int MACSize {
+		get {
+			return h.DigestSize;
+		}
+	}
+
+	/*
+	 * Set the HMAC key. This implies a call to Reset() (thus,
+	 * the key must be set before data begins to be inserted).
+	 */
+	public void SetKey(byte[] key)
+	{
+		SetKey(key, 0, key.Length);
+	}
+
+	/*
+	 * Set the HMAC key ('len' bytes, starting at offset 'off' in key[]).
+	 */
+	public void SetKey(byte[] key, int off, int len)
+	{
+		h.Reset();
+		if (len > h.BlockSize) {
+			h.Update(key, off, len);
+			h.DoFinal(this.key, 0);
+			keyLen = h.DigestSize;
+		} else {
+			Array.Copy(key, off, this.key, 0, len);
+			keyLen = len;
+		}
+		Reset();
+	}
+
+	/*
+	 * Add one byte to the input to HMAC.
+	 */
+	public void Update(byte b)
+	{
+		h.Update(b);
+		dataLen ++;
+	}
+
+	/*
+	 * Add some bytes to the input to HMAC.
+	 */
+	public void Update(byte[] buf)
+	{
+		h.Update(buf, 0, buf.Length);
+	}
+
+	/*
+	 * Add some bytes to the input to HMAC ('len' bytes from buf[],
+	 * starting at offset 'off').
+	 */
+	public void Update(byte[] buf, int off, int len)
+	{
+		h.Update(buf, off, len);
+		dataLen += (ulong)len;
+	}
+
+	/*
+	 * Compute the HMAC value; it is written in outBuf[] at
+	 * offset 'off'. The engine is also reset (as if Reset()
+	 * was called).
+	 */
+	public void DoFinal(byte[] outBuf, int off)
+	{
+		h.DoFinal(tmp, 0);
+		ProcessKey(0x5C);
+		h.Update(tmp);
+		h.DoFinal(outBuf, off);
+		Reset();
+	}
+
+	/*
+	 * Compute the HMAC value; it is written to a newly allocated
+	 * array, which is returned. The engine is also reset (as if
+	 * Reset() was called).
+	 */
+	public byte[] DoFinal()
+	{
+		byte[] r = new byte[h.DigestSize];
+		DoFinal(r, 0);
+		return r;
+	}
+
+	/*
+	 * Reset the HMAC engine. This forgets all previously input
+	 * data, but reuses the currently set key.
+	 */
+	public void Reset()
+	{
+		h.Reset();
+		ProcessKey(0x36);
+		dataLen = 0;
+	}
+
+	/*
+	 * Process some bytes, then compute the output.
+	 *
+	 * This function is supposed to implement the processing in
+	 * constant time (and thus constant memory access pattern) for
+	 * all values of 'len' between 'minLen' and 'maxLen'
+	 * (inclusive). This function works only for the supported
+	 * underlying hash functions (MD5, SHA-1 and the SHA-2
+	 * functions).
+	 *
+	 * The source array (buf[]) must contain at least maxLen bytes
+	 * (starting at offset 'off'); they will all be read.
+	 */
+	public void ComputeCT(byte[] buf, int off, int len,
+		int minLen, int maxLen, byte[] outBuf, int outOff)
+	{
+		/*
+		 * Padding is 0x80, followed by 0 to 63 bytes of value
+		 * 0x00 (up to 127 bytes for SHA-384 and SHA-512), then
+		 * the input bit length expressed over 64 bits
+		 * (little-endian for MD5, big-endian for
+		 * the SHA-* functions)(for SHA-384 and SHA-512, this is
+		 * 128 bits).
+		 *
+		 * Note that we only support bit lengths that fit on
+		 * 64 bits, so we can handle SHA-384/SHA-512 padding
+		 * almost as if it was the same as SHA-256; we just have
+		 * to take care of the larger blocks (128 bytes instead
+		 * of 64) and the larger minimal overhead (17 bytes
+		 * instead of 9 bytes).
+		 *
+		 * be   true for big-endian length encoding
+		 * bs   block size, in bytes (must be a power of 2)
+		 * po   padding overhead (0x80 byte and length encoding)
+		 */
+		bool be;
+		int bs, po;
+		if (h is MD5) {
+			be = false;
+			bs = 64;
+			po = 9;
+		} else if ((h is SHA1) || (h is SHA2Small)) {
+			be = true;
+			bs = 64;
+			po = 9;
+		} else if (h is SHA2Big) {
+			be = true;
+			bs = 128;
+			po = 17;
+		} else {
+			throw new NotSupportedException();
+		}
+
+		/*
+		 * Method implemented here is inspired from the one
+		 * described there:
+		 * https://www.imperialviolet.org/2013/02/04/luckythirteen.html
+		 */
+
+		/*
+		 * We compute the data bit length; let's not forget
+		 * the initial first block (the one with the HMAC key).
+		 */
+		ulong bitLen = ((ulong)bs + dataLen + (ulong)len) << 3;
+
+		/*
+		 * All complete blocks before minLen can be processed
+		 * efficiently.
+		 */
+		ulong nDataLen = (dataLen + (ulong)minLen) & ~(ulong)(bs - 1);
+		if (nDataLen > dataLen) {
+			int zlen = (int)(nDataLen - dataLen);
+			h.Update(buf, off, (int)(nDataLen - dataLen));
+			dataLen = nDataLen;
+			off += zlen;
+			len -= zlen;
+			maxLen -= zlen;
+		}
+
+		/*
+		 * At that point:
+		 * -- dataLen contains the number of bytes already processed
+		 * (in total, not counting the initial key block).
+		 * -- We must input 'len' bytes, which may be up to 'maxLen'
+		 * (inclusive).
+		 *
+		 * We compute kr, kl, kz and km:
+		 *  kr   number of input bytes already in the current block
+		 *  km   index of the first byte after the end of the last
+		 *       padding block, if 'len' is equal to 'maxLen'
+		 *  kz   index of the last byte of the actual last padding
+		 *       block
+		 *  kl   index of the start of the encoded length
+		 */
+		int kr = (int)dataLen & (bs - 1);
+		int kz = ((kr + len + po + bs - 1) & ~(bs - 1)) - 1 - kr;
+		int kl = kz - 7;
+		int km = ((kr + maxLen + po + bs - 1) & ~(bs - 1)) - kr;
+
+		/*
+		 * We must process km bytes. For index i from 0 to km-1:
+		 *   d is from data[] if i < maxLen, 0x00 otherwise
+		 *   e is an encoded length byte or 0x00, depending on i
+		 * These tests do not depend on the actual length, so
+		 * they need not be constant-time.
+		 *
+		 * Actual input byte is:
+		 *   d      if i < len
+		 *   0x80   if i == len
+		 *   0x00   if i > len and i < kl
+		 *   e      if i >= kl
+		 *
+		 * We extract hash state whenever we reach a full block;
+		 * we keep it only if i == kz.
+		 */
+		int hlen = h.DigestSize;
+		for (int k = 0; k < hlen; k ++) {
+			tmp[k] = 0;
+		}
+		for (int i = 0; i < km; i ++) {
+			int d = (i < maxLen) ? buf[off + i] : 0x00;
+			int e;
+			int j = (kr + i) & (bs - 1);
+			if (j >= (bs - 8)) {
+				int k = (j - (bs - 8)) << 3;
+				if (be) {
+					e = (int)(bitLen >> (56 - k));
+				} else {
+					e = (int)(bitLen >> k);
+				}
+				e &= 0xFF;
+			} else {
+				e = 0x00;
+			}
+
+			/*
+			 * x0 is 0x80 if i == len; otherwise it is d.
+			 */
+			int z = i - len;
+			int x0 = 0x80 ^ (((z | -z) >> 31) & (0x80 ^ d));
+
+			/*
+			 * x1 is e if i >= kl; otherwise it is 0x00.
+			 */
+			int x1 = e & ~((i - kl) >> 31);
+
+			/*
+			 * We input x0 if i <= len, x1 otherwise.
+			 */
+			h.Update((byte)(x0 ^ (((len - i) >> 31) & (x0 ^ x1))));
+
+			/*
+			 * Get current state if we are at the end of a block,
+			 * and keep it if i == kz.
+			 */
+			if (j == (bs - 1)) {
+				h.CurrentState(tmpCT, 0);
+				z = i - kz;
+				z = ~((z | -z) >> 31);
+				for (int k = 0; k < hlen; k ++) {
+					tmp[k] |= (byte)(z & tmpCT[k]);
+				}
+			}
+		}
+
+		/*
+		 * We got the hash output in tmp[]; we must complete
+		 * the HMAC computation.
+		 */
+		h.Reset();
+		ProcessKey(0x5C);
+		h.Update(tmp);
+		h.DoFinal(outBuf, outOff);
+		Reset();
+	}
+
+	void ProcessKey(byte pad)
+	{
+		for (int i = 0; i < keyLen; i ++) {
+			h.Update((byte)(key[i] ^ pad));
+		}
+		for (int i = h.BlockSize - keyLen; i > 0; i --) {
+			h.Update(pad);
+		}
+	}
+}
+
+}
diff --git a/Crypto/HMAC_DRBG.cs b/Crypto/HMAC_DRBG.cs
new file mode 100644
index 0000000..abef663
--- /dev/null
+++ b/Crypto/HMAC_DRBG.cs
@@ -0,0 +1,188 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * Implementation of HMAC_DRBG (NIST SP800-90A).
+ *
+ * This class provides HMAC_DRBG as a deterministic PRNG from a given
+ * seed. Once a seed is set, chunks of data are obtained with
+ * GetBytes(). The GetBytes() methods can be called several times;
+ * the internal state is updated after each call. Setting a new seed
+ * resets the internal state.
+ */
+
+public sealed class HMAC_DRBG {
+
+	HMAC hm;
+	byte[] K, V;
+	bool seeded;
+
+	/*
+	 * Create the instance over the provided hash function
+	 * implementation. The digest instance is linked in and will
+	 * be used repeatedly. The engine is not seeded yet.
+	 */
+	public HMAC_DRBG(IDigest h)
+	{
+		hm = new HMAC(h.Dup());
+		int len = h.DigestSize;
+		K = new byte[len];
+		V = new byte[len];
+		seeded = false;
+		Reset();
+	}
+
+	/*
+	 * Reset the engine. A seed will have to be provided before
+	 * generating pseudorandom bytes.
+	 */
+	public void Reset()
+	{
+		for (int i = 0; i < K.Length; i ++) {
+			K[i] = 0x00;
+			V[i] = 0x01;
+		}
+		hm.SetKey(K);
+	}
+
+	/*
+	 * Reset the engine with the provided seed.
+	 */
+	public void SetSeed(byte[] seed)
+	{
+		Reset();
+		Update(seed);
+	}
+
+	/*
+	 * Reset the engine with the provided seed.
+	 */
+	public void SetSeed(byte[] seed, int off, int len)
+	{
+		Reset();
+		Update(seed, off, len);
+	}
+
+	/*
+	 * Inject an additional seed. This may be null, in which case
+	 * the state is modified but the engine is not marked as "seeded"
+	 * (if it was not already marked so).
+	 */
+	public void Update(byte[] seed)
+	{
+		if (seed != null) {
+			Update(seed, 0, seed.Length);
+		} else {
+			Update(null, 0, 0);
+		}
+	}
+
+	/*
+	 * Inject an additional seed. If the seed length is 0, then the
+	 * state is modified, but the engine is not marked as "seeded"
+	 * (if it was not already marked so).
+	 */
+	public void Update(byte[] seed, int off, int len)
+	{
+		/* K = HMAC_K(V || 0x00 || seed) */
+		hm.Update(V);
+		hm.Update((byte)0x00);
+		hm.Update(seed, off, len);
+		hm.DoFinal(K, 0);
+		hm.SetKey(K);
+
+		/* V = HMAC_K(V) */
+		hm.Update(V);
+		hm.DoFinal(V, 0);
+
+		/*
+		 * Stop there if the additional seed is empty.
+		 */
+		if (len == 0) {
+			return;
+		}
+
+		/* K = HMAC_K(V || 0x01 || seed) */
+		hm.Update(V);
+		hm.Update((byte)0x01);
+		hm.Update(seed, off, len);
+		hm.DoFinal(K, 0);
+		hm.SetKey(K);
+
+		/* V = HMAC_K(V) */
+		hm.Update(V);
+		hm.DoFinal(V, 0);
+
+		/*
+		 * We get there only if a non-empty seed is used.
+		 */
+		seeded = true;
+	}
+
+	/*
+	 * Generate some pseudorandom bytes. The engine MUST have been
+	 * seeded.
+	 */
+	public void GetBytes(byte[] buf)
+	{
+		GetBytes(buf, 0, buf.Length);
+	}
+
+	/*
+	 * Generate some pseudorandom bytes. The engine MUST have been
+	 * seeded.
+	 */
+	public void GetBytes(byte[] buf, int off, int len)
+	{
+		if (!seeded) {
+			throw new CryptoException(
+				"HMAC_DRBG engine was not seeded");
+		}
+		while (len > 0) {
+			/* V = HMAC_K(V) */
+			hm.Update(V);
+			hm.DoFinal(V, 0);
+			int clen = Math.Min(V.Length, len);
+			Array.Copy(V, 0, buf, off, clen);
+			off += clen;
+			len -= clen;
+		}
+
+		/* K = HMAC_K(V || 0x00) */
+		hm.Update(V);
+		hm.Update((byte)0x00);
+		hm.DoFinal(K, 0);
+		hm.SetKey(K);
+
+		/* V = HMAC_K(V) */
+		hm.Update(V);
+		hm.DoFinal(V, 0);
+	}
+}
+
+}
diff --git a/Crypto/IBlockCipher.cs b/Crypto/IBlockCipher.cs
new file mode 100644
index 0000000..6a51b99
--- /dev/null
+++ b/Crypto/IBlockCipher.cs
@@ -0,0 +1,149 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * Interface for a block cipher implementation. Each instance has a
+ * state, which contains (at least) the current secret key, but may also
+ * have other internal values. Thereby, instances are not thread-safe.
+ * The Dup() method may be used to "clone" an instance into a new,
+ * independent instance that starts its life configured with the same
+ * secret key.
+ */
+
+public interface IBlockCipher {
+
+	/*
+	 * Get the block size in bytes.
+	 */
+	int BlockSize { get; }
+
+	/*
+	 * Set the key.
+	 */
+	void SetKey(byte[] key);
+
+	/*
+	 * Set the key.
+	 */
+	void SetKey(byte[] key, int off, int len);
+
+	/*
+	 * Encrypt one block.
+	 */
+	void BlockEncrypt(byte[] buf);
+
+	/*
+	 * Encrypt one block.
+	 */
+	void BlockEncrypt(byte[] buf, int off);
+
+	/*
+	 * Decrypt one block.
+	 */
+	void BlockDecrypt(byte[] buf);
+
+	/*
+	 * Encrypt one block.
+	 */
+	void BlockDecrypt(byte[] buf, int off);
+
+	/*
+	 * Do CBC encryption. There is no padding; the source array
+	 * must already have a length multiple of the block size.
+	 * The provided iv[] array must have the same length as a
+	 * block. The data is encrypted in-place. The iv[] array is
+	 * unmodified.
+	 */
+	void CBCEncrypt(byte[] iv, byte[] data);
+
+	/*
+	 * Do CBC encryption. There is no padding; the source array
+	 * must already have a length multiple of the block size.
+	 * The provided iv[] array must have the same length as a
+	 * block. The data is encrypted in-place. The iv[] array is
+	 * unmodified.
+	 */
+	void CBCEncrypt(byte[] iv, byte[] data, int off, int len);
+
+	/*
+	 * Do CBC decryption. The source array must have a length
+	 * multiple of the block size; no attempt at padding removal is
+	 * performed. The provided iv[] array must have the same length
+	 * as a block. The data is decrypted in-place. The iv[] array is
+	 * unmodified.
+	 */
+	void CBCDecrypt(byte[] iv, byte[] data);
+
+	/*
+	 * Do CBC decryption. The source array must have a length
+	 * multiple of the block size; no attempt at padding removal is
+	 * performed. The provided iv[] array must have the same length
+	 * as a block. The data is decrypted in-place. The iv[] array is
+	 * unmodified.
+	 */
+	void CBCDecrypt(byte[] iv, byte[] data, int off, int len);
+
+	/*
+	 * Do CTR encryption or decryption. This implements the variant
+	 * used in GCM:
+	 *  - IV length is 4 bytes less than the block length
+	 *  - The block counter is used for the last 4 bytes of the
+	 *    block input; big-endian encoding is used.
+	 *  - Counter arithmetic is done modulo 2^32.
+	 *
+	 * The starting counter value is provided as parameter; the new
+	 * counter value is returned. This allows computing a long CTR
+	 * run in several chunks, as long as all chunks (except possibly
+	 * the last one) have a length which is multiple of the block size.
+	 */
+	uint CTRRun(byte[] iv, uint cc, byte[] data);
+
+	/*
+	 * Do CTR encryption or decryption. This implements the variant
+	 * used in GCM:
+	 *  - IV length is 4 bytes less than the block length
+	 *  - The block counter is used for the last 4 bytes of the
+	 *    block input; big-endian encoding is used.
+	 *  - Counter arithmetic is done modulo 2^32.
+	 *
+	 * The starting counter value is provided as parameter; the new
+	 * counter value is returned. This allows computing a long CTR
+	 * run in several chunks, as long as all chunks (except possibly
+	 * the last one) have a length which is multiple of the block size.
+	 */
+	uint CTRRun(byte[] iv, uint cc, byte[] data, int off, int len);
+
+	/*
+	 * Duplicate this engine. This creates a new, independent
+	 * instance that implements the same function, and starts with
+	 * the same currently set key.
+	 */
+	IBlockCipher Dup();
+}
+
+}
diff --git a/Crypto/IDigest.cs b/Crypto/IDigest.cs
new file mode 100644
index 0000000..87fb4bf
--- /dev/null
+++ b/Crypto/IDigest.cs
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * This interface qualifies a hash function implementation.
+ */
+
+public interface IDigest {
+
+	/*
+	 * Get the hash function symbolic name.
+	 */
+	string Name { get; }
+
+	/*
+	 * Get the hash function output size, in bytes.
+	 */
+	int DigestSize { get; }
+
+	/*
+	 * Get the hash function block size, in bytes (the block
+	 * size is used in the definition of HMAC).
+	 */
+	int BlockSize { get; }
+
+	/*
+	 * Add one byte to the current input.
+	 */
+	void Update(byte b);
+
+	/*
+	 * Add some bytes to the current input.
+	 */
+	void Update(byte[] buf);
+
+	/*
+	 * Add some bytes to the current input ('len' bytes from buf[],
+	 * starting at offset 'off').
+	 */
+	void Update(byte[] buf, int off, int len);
+
+	/*
+	 * Finalize the hash computation and write the output in the
+	 * provided outBuf[] array (starting at offset 'off'). This
+	 * instance is also automatically reset (as if by a Reset() call).
+	 */
+	void DoFinal(byte[] outBuf, int off);
+
+	/*
+	 * Finalize the hash computation and write the output into a
+	 * newly allocated buffer, which is returned. This instance
+	 * is also automatically reset (as if by a Reset() call).
+	 */
+	byte[] DoFinal();
+
+	/*
+	 * Finalize the current hash computation but keep it active;
+	 * this thus returns the hash value computed over the input
+	 * bytes injected so far, but new bytes may be added afterwards.
+	 * The output is written in outBuf[] at offset 'off'.
+	 */
+	void DoPartial(byte[] outBuf, int off);
+
+	/*
+	 * Finalize the current hash computation but keep it active;
+	 * this thus returns the hash value computed over the input
+	 * bytes injected so far, but new bytes may be added afterwards.
+	 * The output is written into a newly allocated array, which
+	 * is returned.
+	 */
+	byte[] DoPartial();
+
+	/*
+	 * Encode the current running state into the provided buffer.
+	 * This is defined for functions that employ an internal padding
+	 * but no special finalization step (e.g. MD5, SHA-1, SHA-256);
+	 * the running state is the one resulting from the last
+	 * processed block.
+	 *
+	 * Note: for SHA-224, SHA-384 and similar functions, the current
+	 * state as returned by this method will be truncated to the
+	 * actual hash output length.
+	 */
+	void CurrentState(byte[] outBuf, int off);
+
+	/*
+	 * Reset the internal state, to start a new computation. This
+	 * can be called at any time.
+	 */
+	void Reset();
+
+	/*
+	 * Duplicate this engine. This creates a new, independent
+	 * instance that implements the same function, and starts with
+	 * the current state.
+	 */
+	IDigest Dup();
+
+	/*
+	 * Compute the hash of a given input in one call; this does NOT
+	 * change the internal state.
+	 */
+	byte[] Hash(byte[] buf);
+
+	/*
+	 * Compute the hash of a given input in one call; this does NOT
+	 * change the internal state.
+	 */
+	byte[] Hash(byte[] buf, int off, int len);
+}
+
+}
diff --git a/Crypto/IPrivateKey.cs b/Crypto/IPrivateKey.cs
new file mode 100644
index 0000000..f29f81d
--- /dev/null
+++ b/Crypto/IPrivateKey.cs
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * Interface of an asymmetric private key.
+ */
+
+public interface IPrivateKey {
+
+	/*
+	 * Private key size, expressed in bits. Exact definition of the
+	 * "size" depends on the key type.
+	 */
+	int KeySizeBits { get; }
+
+	/*
+	 * Algorithm name (for display).
+	 */
+	string AlgorithmName { get; }
+
+	/*
+	 * Get (or compute) the public key for this private key.
+	 */
+	IPublicKey PublicKey { get; }
+
+	/*
+	 * Perform algorithm-dependent consistency checks. These
+	 * checks may be relatively expensive. On any error, a
+	 * CryptoException is thrown.
+	 */
+	void CheckValid();
+}
+
+}
diff --git a/Crypto/IPublicKey.cs b/Crypto/IPublicKey.cs
new file mode 100644
index 0000000..150e190
--- /dev/null
+++ b/Crypto/IPublicKey.cs
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * Interface for an asymmetric public key.
+ */
+
+public interface IPublicKey {
+
+	/*
+	 * Public key size, expressed in bits. Exact definition of the
+	 * "size" depends on the key type.
+	 */
+	int KeySizeBits { get; }
+
+	/*
+	 * Algorithm name (for display).
+	 */
+	string AlgorithmName { get; }
+
+	/*
+	 * Perform algorithm-dependent consistency checks. These
+	 * checks may be relatively expensive. On any error, a
+	 * CryptoException is thrown.
+	 */
+	void CheckValid();
+
+	/*
+	 * Public keys ought to be comparable for equality.
+	 */
+	bool Equals(object obj);
+	int GetHashCode();
+}
+
+}
diff --git a/Crypto/MD5.cs b/Crypto/MD5.cs
new file mode 100644
index 0000000..7b083f3
--- /dev/null
+++ b/Crypto/MD5.cs
@@ -0,0 +1,391 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * MD5 implementation. MD5 is described in RFC 1321.
+ *
+ * WARNING: as a cryptographic hash function, MD5 turned out to be
+ * very weak with regards to collisions. Use with care.
+ */
+
+public sealed class MD5 : DigestCore {
+
+	const int BLOCK_LEN = 64;
+
+	uint A, B, C, D;
+	byte[] block, saveBlock;
+	int ptr;
+	ulong byteCount;
+
+	/*
+	 * Create a new instance. It is ready to process data bytes.
+	 */
+	public MD5()
+	{
+		block = new byte[BLOCK_LEN];
+		saveBlock = new byte[BLOCK_LEN];
+		Reset();
+	}
+
+	/* see IDigest */
+	public override string Name {
+		get {
+			return "MD5";
+		}
+	}
+
+	/* see IDigest */
+	public override int DigestSize {
+		get {
+			return 16;
+		}
+	}
+
+	/* see IDigest */
+	public override int BlockSize {
+		get {
+			return 64;
+		}
+	}
+
+	/* see IDigest */
+	public override void Reset()
+	{
+		A = 0x67452301;
+		B = 0xEFCDAB89;
+		C = 0x98BADCFE;
+		D = 0x10325476;
+		byteCount = 0;
+		ptr = 0;
+	}
+
+	/* see IDigest */
+	public override void Update(byte b)
+	{
+		block[ptr ++] = b;
+		byteCount ++;
+		if (ptr == BLOCK_LEN) {
+			ProcessBlock();
+		}
+	}
+
+	/* see IDigest */
+	public override void Update(byte[] buf, int off, int len)
+	{
+		if (len < 0) {
+			throw new ArgumentException("negative chunk length");
+		}
+		byteCount += (ulong)len;
+		while (len > 0) {
+			int clen = Math.Min(len, BLOCK_LEN - ptr);
+			Array.Copy(buf, off, block, ptr, clen);
+			off += clen;
+			len -= clen;
+			ptr += clen;
+			if (ptr == BLOCK_LEN) {
+				ProcessBlock();
+			}
+		}
+	}
+
+	/* see IDigest */
+	public override void DoPartial(byte[] outBuf, int off)
+	{
+		/*
+		 * Save current state.
+		 */
+		uint saveA = A;
+		uint saveB = B;
+		uint saveC = C;
+		uint saveD = D;
+		int savePtr = ptr;
+		Array.Copy(block, 0, saveBlock, 0, savePtr);
+
+		/*
+		 * Add padding. This may involve processing an extra block.
+		 */
+		block[ptr ++] = 0x80;
+		if (ptr > BLOCK_LEN - 8) {
+			for (int j = ptr; j < BLOCK_LEN; j ++) {
+				block[j] = 0;
+			}
+			ProcessBlock();
+		}
+		for (int j = ptr; j < (BLOCK_LEN - 8); j ++) {
+			block[j] = 0;
+		}
+		ulong x = byteCount << 3;
+		Enc32le((uint)x, block, BLOCK_LEN - 8);
+		Enc32le((uint)(x >> 32), block, BLOCK_LEN - 4);
+
+		/*
+		 * Process final block and encode result.
+		 */
+		ProcessBlock();
+		Enc32le(A, outBuf, off);
+		Enc32le(B, outBuf, off + 4);
+		Enc32le(C, outBuf, off + 8);
+		Enc32le(D, outBuf, off + 12);
+
+		/*
+		 * Restore current state.
+		 */
+		Array.Copy(saveBlock, 0, block, 0, savePtr);
+		A = saveA;
+		B = saveB;
+		C = saveC;
+		D = saveD;
+		ptr = savePtr;
+	}
+
+	/* see IDigest */
+	public override IDigest Dup()
+	{
+		MD5 h = new MD5();
+		h.A = A;
+		h.B = B;
+		h.C = C;
+		h.D = D;
+		h.ptr = ptr;
+		h.byteCount = byteCount;
+		Array.Copy(block, 0, h.block, 0, ptr);
+		return h;
+	}
+
+	/* see IDigest */
+	public override void CurrentState(byte[] outBuf, int off)
+	{
+		Enc32le(A, outBuf, off);
+		Enc32le(B, outBuf, off + 4);
+		Enc32le(C, outBuf, off + 8);
+		Enc32le(D, outBuf, off + 12);
+	}
+
+	void ProcessBlock()
+	{
+		/*
+		 * Decode input block (sixteen 32-bit words).
+		 */
+		uint x0 = Dec32le(block,  0);
+		uint x1 = Dec32le(block,  4);
+		uint x2 = Dec32le(block,  8);
+		uint x3 = Dec32le(block, 12);
+		uint x4 = Dec32le(block, 16);
+		uint x5 = Dec32le(block, 20);
+		uint x6 = Dec32le(block, 24);
+		uint x7 = Dec32le(block, 28);
+		uint x8 = Dec32le(block, 32);
+		uint x9 = Dec32le(block, 36);
+		uint xA = Dec32le(block, 40);
+		uint xB = Dec32le(block, 44);
+		uint xC = Dec32le(block, 48);
+		uint xD = Dec32le(block, 52);
+		uint xE = Dec32le(block, 56);
+		uint xF = Dec32le(block, 60);
+
+		/*
+		 * Read state words.
+		 */
+		uint wa = A;
+		uint wb = B;
+		uint wc = C;
+		uint wd = D;
+		uint tmp;
+
+		/*
+		 * Rounds 0 to 15.
+		 */
+		tmp = wa + (wd ^ (wb & (wc ^ wd))) + x0 + 0xD76AA478;
+		wa = wb + ((tmp << 7) | (tmp >> 25));
+		tmp = wd + (wc ^ (wa & (wb ^ wc))) + x1 + 0xE8C7B756;
+		wd = wa + ((tmp << 12) | (tmp >> 20));
+		tmp = wc + (wb ^ (wd & (wa ^ wb))) + x2 + 0x242070DB;
+		wc = wd + ((tmp << 17) | (tmp >> 15));
+		tmp = wb + (wa ^ (wc & (wd ^ wa))) + x3 + 0xC1BDCEEE;
+		wb = wc + ((tmp << 22) | (tmp >> 10));
+		tmp = wa + (wd ^ (wb & (wc ^ wd))) + x4 + 0xF57C0FAF;
+		wa = wb + ((tmp << 7) | (tmp >> 25));
+		tmp = wd + (wc ^ (wa & (wb ^ wc))) + x5 + 0x4787C62A;
+		wd = wa + ((tmp << 12) | (tmp >> 20));
+		tmp = wc + (wb ^ (wd & (wa ^ wb))) + x6 + 0xA8304613;
+		wc = wd + ((tmp << 17) | (tmp >> 15));
+		tmp = wb + (wa ^ (wc & (wd ^ wa))) + x7 + 0xFD469501;
+		wb = wc + ((tmp << 22) | (tmp >> 10));
+		tmp = wa + (wd ^ (wb & (wc ^ wd))) + x8 + 0x698098D8;
+		wa = wb + ((tmp << 7) | (tmp >> 25));
+		tmp = wd + (wc ^ (wa & (wb ^ wc))) + x9 + 0x8B44F7AF;
+		wd = wa + ((tmp << 12) | (tmp >> 20));
+		tmp = wc + (wb ^ (wd & (wa ^ wb))) + xA + 0xFFFF5BB1;
+		wc = wd + ((tmp << 17) | (tmp >> 15));
+		tmp = wb + (wa ^ (wc & (wd ^ wa))) + xB + 0x895CD7BE;
+		wb = wc + ((tmp << 22) | (tmp >> 10));
+		tmp = wa + (wd ^ (wb & (wc ^ wd))) + xC + 0x6B901122;
+		wa = wb + ((tmp << 7) | (tmp >> 25));
+		tmp = wd + (wc ^ (wa & (wb ^ wc))) + xD + 0xFD987193;
+		wd = wa + ((tmp << 12) | (tmp >> 20));
+		tmp = wc + (wb ^ (wd & (wa ^ wb))) + xE + 0xA679438E;
+		wc = wd + ((tmp << 17) | (tmp >> 15));
+		tmp = wb + (wa ^ (wc & (wd ^ wa))) + xF + 0x49B40821;
+		wb = wc + ((tmp << 22) | (tmp >> 10));
+
+		/*
+		 * Rounds 16 to 31.
+		 */
+		tmp = wa + (wc ^ (wd & (wb ^ wc))) + x1 + 0xF61E2562;
+		wa = wb + ((tmp << 5) | (tmp >> 27));
+		tmp = wd + (wb ^ (wc & (wa ^ wb))) + x6 + 0xC040B340;
+		wd = wa + ((tmp << 9) | (tmp >> 23));
+		tmp = wc + (wa ^ (wb & (wd ^ wa))) + xB + 0x265E5A51;
+		wc = wd + ((tmp << 14) | (tmp >> 18));
+		tmp = wb + (wd ^ (wa & (wc ^ wd))) + x0 + 0xE9B6C7AA;
+		wb = wc + ((tmp << 20) | (tmp >> 12));
+		tmp = wa + (wc ^ (wd & (wb ^ wc))) + x5 + 0xD62F105D;
+		wa = wb + ((tmp << 5) | (tmp >> 27));
+		tmp = wd + (wb ^ (wc & (wa ^ wb))) + xA + 0x02441453;
+		wd = wa + ((tmp << 9) | (tmp >> 23));
+		tmp = wc + (wa ^ (wb & (wd ^ wa))) + xF + 0xD8A1E681;
+		wc = wd + ((tmp << 14) | (tmp >> 18));
+		tmp = wb + (wd ^ (wa & (wc ^ wd))) + x4 + 0xE7D3FBC8;
+		wb = wc + ((tmp << 20) | (tmp >> 12));
+		tmp = wa + (wc ^ (wd & (wb ^ wc))) + x9 + 0x21E1CDE6;
+		wa = wb + ((tmp << 5) | (tmp >> 27));
+		tmp = wd + (wb ^ (wc & (wa ^ wb))) + xE + 0xC33707D6;
+		wd = wa + ((tmp << 9) | (tmp >> 23));
+		tmp = wc + (wa ^ (wb & (wd ^ wa))) + x3 + 0xF4D50D87;
+		wc = wd + ((tmp << 14) | (tmp >> 18));
+		tmp = wb + (wd ^ (wa & (wc ^ wd))) + x8 + 0x455A14ED;
+		wb = wc + ((tmp << 20) | (tmp >> 12));
+		tmp = wa + (wc ^ (wd & (wb ^ wc))) + xD + 0xA9E3E905;
+		wa = wb + ((tmp << 5) | (tmp >> 27));
+		tmp = wd + (wb ^ (wc & (wa ^ wb))) + x2 + 0xFCEFA3F8;
+		wd = wa + ((tmp << 9) | (tmp >> 23));
+		tmp = wc + (wa ^ (wb & (wd ^ wa))) + x7 + 0x676F02D9;
+		wc = wd + ((tmp << 14) | (tmp >> 18));
+		tmp = wb + (wd ^ (wa & (wc ^ wd))) + xC + 0x8D2A4C8A;
+		wb = wc + ((tmp << 20) | (tmp >> 12));
+
+		/*
+		 * Rounds 32 to 47.
+		 */
+		tmp = wa + (wb ^ wc ^ wd) + x5 + 0xFFFA3942;
+		wa = wb + ((tmp << 4) | (tmp >> 28));
+		tmp = wd + (wa ^ wb ^ wc) + x8 + 0x8771F681;
+		wd = wa + ((tmp << 11) | (tmp >> 21));
+		tmp = wc + (wd ^ wa ^ wb) + xB + 0x6D9D6122;
+		wc = wd + ((tmp << 16) | (tmp >> 16));
+		tmp = wb + (wc ^ wd ^ wa) + xE + 0xFDE5380C;
+		wb = wc + ((tmp << 23) | (tmp >> 9));
+		tmp = wa + (wb ^ wc ^ wd) + x1 + 0xA4BEEA44;
+		wa = wb + ((tmp << 4) | (tmp >> 28));
+		tmp = wd + (wa ^ wb ^ wc) + x4 + 0x4BDECFA9;
+		wd = wa + ((tmp << 11) | (tmp >> 21));
+		tmp = wc + (wd ^ wa ^ wb) + x7 + 0xF6BB4B60;
+		wc = wd + ((tmp << 16) | (tmp >> 16));
+		tmp = wb + (wc ^ wd ^ wa) + xA + 0xBEBFBC70;
+		wb = wc + ((tmp << 23) | (tmp >> 9));
+		tmp = wa + (wb ^ wc ^ wd) + xD + 0x289B7EC6;
+		wa = wb + ((tmp << 4) | (tmp >> 28));
+		tmp = wd + (wa ^ wb ^ wc) + x0 + 0xEAA127FA;
+		wd = wa + ((tmp << 11) | (tmp >> 21));
+		tmp = wc + (wd ^ wa ^ wb) + x3 + 0xD4EF3085;
+		wc = wd + ((tmp << 16) | (tmp >> 16));
+		tmp = wb + (wc ^ wd ^ wa) + x6 + 0x04881D05;
+		wb = wc + ((tmp << 23) | (tmp >> 9));
+		tmp = wa + (wb ^ wc ^ wd) + x9 + 0xD9D4D039;
+		wa = wb + ((tmp << 4) | (tmp >> 28));
+		tmp = wd + (wa ^ wb ^ wc) + xC + 0xE6DB99E5;
+		wd = wa + ((tmp << 11) | (tmp >> 21));
+		tmp = wc + (wd ^ wa ^ wb) + xF + 0x1FA27CF8;
+		wc = wd + ((tmp << 16) | (tmp >> 16));
+		tmp = wb + (wc ^ wd ^ wa) + x2 + 0xC4AC5665;
+		wb = wc + ((tmp << 23) | (tmp >> 9));
+
+		/*
+		 * Rounds 48 to 63.
+		 */
+		tmp = wa + (wc ^ (wb | ~wd)) + x0 + 0xF4292244;
+		wa = wb + ((tmp << 6) | (tmp >> 26));
+		tmp = wd + (wb ^ (wa | ~wc)) + x7 + 0x432AFF97;
+		wd = wa + ((tmp << 10) | (tmp >> 22));
+		tmp = wc + (wa ^ (wd | ~wb)) + xE + 0xAB9423A7;
+		wc = wd + ((tmp << 15) | (tmp >> 17));
+		tmp = wb + (wd ^ (wc | ~wa)) + x5 + 0xFC93A039;
+		wb = wc + ((tmp << 21) | (tmp >> 11));
+		tmp = wa + (wc ^ (wb | ~wd)) + xC + 0x655B59C3;
+		wa = wb + ((tmp << 6) | (tmp >> 26));
+		tmp = wd + (wb ^ (wa | ~wc)) + x3 + 0x8F0CCC92;
+		wd = wa + ((tmp << 10) | (tmp >> 22));
+		tmp = wc + (wa ^ (wd | ~wb)) + xA + 0xFFEFF47D;
+		wc = wd + ((tmp << 15) | (tmp >> 17));
+		tmp = wb + (wd ^ (wc | ~wa)) + x1 + 0x85845DD1;
+		wb = wc + ((tmp << 21) | (tmp >> 11));
+		tmp = wa + (wc ^ (wb | ~wd)) + x8 + 0x6FA87E4F;
+		wa = wb + ((tmp << 6) | (tmp >> 26));
+		tmp = wd + (wb ^ (wa | ~wc)) + xF + 0xFE2CE6E0;
+		wd = wa + ((tmp << 10) | (tmp >> 22));
+		tmp = wc + (wa ^ (wd | ~wb)) + x6 + 0xA3014314;
+		wc = wd + ((tmp << 15) | (tmp >> 17));
+		tmp = wb + (wd ^ (wc | ~wa)) + xD + 0x4E0811A1;
+		wb = wc + ((tmp << 21) | (tmp >> 11));
+		tmp = wa + (wc ^ (wb | ~wd)) + x4 + 0xF7537E82;
+		wa = wb + ((tmp << 6) | (tmp >> 26));
+		tmp = wd + (wb ^ (wa | ~wc)) + xB + 0xBD3AF235;
+		wd = wa + ((tmp << 10) | (tmp >> 22));
+		tmp = wc + (wa ^ (wd | ~wb)) + x2 + 0x2AD7D2BB;
+		wc = wd + ((tmp << 15) | (tmp >> 17));
+		tmp = wb + (wd ^ (wc | ~wa)) + x9 + 0xEB86D391;
+		wb = wc + ((tmp << 21) | (tmp >> 11));
+
+		/*
+		 * Update state words and reset block pointer.
+		 */
+		A += wa;
+		B += wb;
+		C += wc;
+		D += wd;
+		ptr = 0;
+	}
+
+	static uint Dec32le(byte[] buf, int off)
+	{
+		return (uint)buf[off]
+			| ((uint)buf[off + 1] << 8)
+			| ((uint)buf[off + 2] << 16)
+			| ((uint)buf[off + 3] << 24);
+	}
+
+	static void Enc32le(uint x, byte[] buf, int off)
+	{
+		buf[off] = (byte)x;
+		buf[off + 1] = (byte)(x >> 8);
+		buf[off + 2] = (byte)(x >> 16);
+		buf[off + 3] = (byte)(x >> 24);
+	}
+}
+
+}
diff --git a/Crypto/ModInt.cs b/Crypto/ModInt.cs
new file mode 100644
index 0000000..2aa952a
--- /dev/null
+++ b/Crypto/ModInt.cs
@@ -0,0 +1,1122 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * Mutable container for a modular integer.
+ *
+ * Rules:
+ *
+ *   - Each instance is initialised over a given modulus, or by
+ *     duplicating an existing instance.
+ *
+ *   - All operands for a given operation must share the same modulus,
+ *     i.e. the instances must have been created with Dup() calls from
+ *     a common ancestor.
+ *
+ *   - Modulus must be odd and greater than 1.
+ */
+
+public class ModInt {
+
+	/*
+	 * Dedicated class for a modulus. It maintains some useful
+	 * values that depend only on the modulus.
+	 */
+	class ZMod {
+
+		/*
+		 * val = modulus value, 31 bits per word, little-endian
+		 * bitLen = modulus bit length (exact)
+		 * n0i is such that n0i * val[0] == -1 mod 2^31
+		 * R = 2^(31*val.Length) (modular)
+		 * R2 = 2^(31*(val.Length*2)) (modular)
+		 * Rx[i] = 2^(31*(val.Length+(2^i))) (modular)
+		 */
+		internal uint[] val;
+		internal int bitLen;
+		internal uint n0i;
+		internal uint[] R;
+		internal uint[] R2;
+		internal uint[][] Rx;
+		internal byte[] vm2;
+
+		internal ZMod(byte[] bmod, int off, int len)
+		{
+			bitLen = BigInt.BitLength(bmod, off, len);
+			if (bitLen <= 1 || (bmod[off + len - 1] & 1) == 0) {
+				throw new CryptoException("invalid modulus");
+			}
+			int n = (bitLen + 30) / 31;
+			val = new uint[n];
+			DecodeBE(bmod, off, len, val);
+			uint x = val[0];
+			uint y = 2 - x;
+			y *= 2 - y * x;
+			y *= 2 - y * x;
+			y *= 2 - y * x;
+			y *= 2 - y * x;
+			n0i = (1 + ~y) & 0x7FFFFFFF;
+
+			/*
+			 * Compute the Rx[] values.
+			 */
+			int zk = 0;
+			while ((n >> zk) != 0) {
+				zk ++;
+			}
+			R = new uint[n];
+			R[n - 1] = 1;
+			for (int i = 0; i < 31; i ++) {
+				ModMul2(R, val);
+			}
+			Rx = new uint[zk][];
+			Rx[0] = new uint[n];
+			Array.Copy(R, 0, Rx[0], 0, n);
+			for (int i = 0; i < 31; i ++) {
+				ModMul2(Rx[0], val);
+			}
+			for (int k = 1; k < zk; k ++) {
+				Rx[k] = new uint[n];
+				MontyMul(Rx[k - 1], Rx[k - 1], Rx[k], val, n0i);
+			}
+
+			/*
+			 * Compute R2 by multiplying the relevant Rx[]
+			 * values.
+			 */
+			R2 = null;
+			uint[] tt = new uint[n];
+			for (int k = 0; k < zk; k ++) {
+				if (((n >> k) & 1) == 0) {
+					continue;
+				}
+				if (R2 == null) {
+					R2 = new uint[n];
+					Array.Copy(Rx[k], 0, R2, 0, n);
+				} else {
+					MontyMul(Rx[k], R2, tt, val, n0i);
+					Array.Copy(tt, 0, R2, 0, n);
+				}
+			}
+
+			/*
+			 * Compute N-2; used as an exponent for modular
+			 * inversion. Since modulus N is odd and greater
+			 * than 1, N-2 is necessarily positive.
+			 */
+			vm2 = new byte[(bitLen + 7) >> 3];
+			int cc = 2;
+			for (int i = 0; i < vm2.Length; i ++) {
+				int b = bmod[off + len - 1 - i];
+				b -= cc;
+				vm2[vm2.Length - 1 - i] = (byte)b;
+				cc = (b >> 8) & 1;
+			}
+		}
+	}
+
+	ZMod mod;
+	uint[] val;
+	uint[] tmp1, tmp2;
+
+	/*
+	 * Get the modulus bit length.
+	 */
+	public int ModBitLength {
+		get {
+			return mod.bitLen;
+		}
+	}
+
+	/*
+	 * Test whether this value is zero.
+	 */
+	public bool IsZero {
+		get {
+			return IsZeroCT != 0;
+		}
+	}
+
+	public uint IsZeroCT {
+		get {
+			int z = (int)val[0];
+			for (int i = 1; i < val.Length; i ++) {
+				z |= (int)val[i];
+			}
+			return ~(uint)((z | -z) >> 31);
+		}
+	}
+
+	/*
+	 * Test whether this value is one.
+	 */
+	public bool IsOne {
+		get {
+			return IsOneCT != 0;
+		}
+	}
+
+	public uint IsOneCT {
+		get {
+			int z = (int)val[0] ^ 1;
+			for (int i = 1; i < val.Length; i ++) {
+				z |= (int)val[i];
+			}
+			return ~(uint)((z | -z) >> 31);
+		}
+	}
+
+	ModInt(ZMod m)
+	{
+		Init(m);
+	}
+
+	/*
+	 * Create a new instance by decoding the provided modulus
+	 * (unsigned big-endian). Value is zero.
+	 */
+	public ModInt(byte[] modulus)
+		: this(modulus, 0, modulus.Length)
+	{
+	}
+
+	/*
+	 * Create a new instance by decoding the provided modulus
+	 * (unsigned big-endian). Value is zero.
+	 */
+	public ModInt(byte[] modulus, int off, int len)
+	{
+		Init(new ZMod(modulus, off, len));
+	}
+
+	void Init(ZMod mod)
+	{
+		this.mod = mod;
+		int n = mod.val.Length;
+		val = new uint[n];
+		tmp1 = new uint[n];
+		tmp2 = new uint[n];
+	}
+
+	/*
+	 * Duplicate this instance. The new instance uses the same
+	 * modulus, and its value is initialized to the same value as
+	 * this instance.
+	 */
+	public ModInt Dup()
+	{
+		ModInt m = new ModInt(mod);
+		Array.Copy(val, 0, m.val, 0, val.Length);
+		return m;
+	}
+
+	/*
+	 * Set the value in this instance to a copy of the value in
+	 * the provided instance. The 'm' instance may use a different
+	 * modulus, in which case the value may incur modular reduction.
+	 */
+	public void Set(ModInt m)
+	{
+		/*
+		 * If the instances use the same modulus array, then
+		 * the value can simply be copied as is.
+		 */
+		if (mod == m.mod) {
+			Array.Copy(m.val, 0, val, 0, val.Length);
+			return;
+		}
+		Reduce(m.val);
+	}
+
+	/*
+	 * Set the value in this instance to a copy of the value in
+	 * the provided instance, but do so only conditionally. If
+	 * 'ctl' is -1, then the copy is done. If 'ctl' is 0, then the
+	 * copy is NOT done. This instance and the source instance
+	 * MUST use the same modulus.
+	 */
+	public void CondCopy(ModInt m, uint ctl)
+	{
+		if (mod != m.mod) {
+			throw new CryptoException("Not same modulus");
+		}
+		for (int i = 0; i < val.Length; i ++) {
+			uint w = val[i];
+			val[i] = w ^ (ctl & (m.val[i] ^ w));
+		}
+	}
+
+	/*
+	 * Set the value in this instance to a copy of either 'a'
+	 * (if ctl is -1) or 'b' (if ctl is 0).
+	 */
+	public void CopyMux(uint ctl, ModInt a, ModInt b)
+	{
+		if (mod != a.mod || mod != b.mod) {
+			throw new CryptoException("Not same modulus");
+		}
+		for (int i = 0; i < val.Length; i ++) {
+			uint aw = a.val[i];
+			uint bw = b.val[i];
+			val[i] = bw ^ (ctl & (aw ^ bw));
+		}
+	}
+
+	/*
+	 * Set the value in this instance to the provided integer value
+	 * (which MUST be nonnegative).
+	 */
+	public void Set(int x)
+	{
+		val[0] = (uint)x;
+		for (int i = 1; i < val.Length; i ++) {
+			val[i] = 0;
+		}
+	}
+
+	/*
+	 * Set this value to either 0 (if ctl is 0) or 1 (if ctl is -1),
+	 * in Montgomery representation.
+	 */
+	public void SetMonty(uint ctl)
+	{
+		for (int i = 0; i < val.Length; i ++) {
+			val[i] = ctl & mod.R[i];
+		}
+	}
+
+	/*
+	 * Set this value by decoding the provided integer (big-endian
+	 * unsigned encoding). If the source value does not fit (it is
+	 * not lower than the modulus), then this method return 0, and
+	 * this instance is set to 0. Otherwise, it returns -1.
+	 */
+	public uint Decode(byte[] buf)
+	{
+		return Decode(buf, 0, buf.Length);
+	}
+
+	/*
+	 * Set this value by decoding the provided integer (big-endian
+	 * unsigned encoding). If the source value does not fit (it is
+	 * not lower than the modulus), then this method return 0, and
+	 * this instance is set to 0. Otherwise, it returns -1.
+	 */
+	public uint Decode(byte[] buf, int off, int len)
+	{
+		/*
+		 * Decode into val[]; if the truncation loses some
+		 * non-zero bytes, then this returns 0.
+		 */
+		uint x = DecodeBE(buf, off, len, val);
+
+		/*
+		 * Compare with modulus. We want to get a 0 if the
+		 * subtraction would not yield a carry, i.e. the
+		 * source value is not lower than the modulus.
+		 */
+		x &= Sub(val, mod.val, 0);
+
+		/*
+		 * At that point, x is -1 if the value fits, 0
+		 * otherwise.
+		 */
+		for (int i = 0; i < val.Length; i ++) {
+			val[i] &= x;
+		}
+		return x;
+	}
+
+	/*
+	 * Set this value by decoding the provided integer (big-endian
+	 * unsigned encoding). The source value is reduced if necessary.
+	 */
+	public void DecodeReduce(byte[] buf)
+	{
+		DecodeReduce(buf, 0, buf.Length);
+	}
+
+	/*
+	 * Set this value by decoding the provided integer (big-endian
+	 * unsigned encoding). The source value is reduced if necessary.
+	 */
+	public void DecodeReduce(byte[] buf, int off, int len)
+	{
+		uint[] x = new uint[((len << 3) + 30) / 31];
+		DecodeBE(buf, off, len, x);
+		Reduce(x);
+	}
+
+	/*
+	 * Set the value from the provided source array, with modular
+	 * reduction.
+	 */
+	void Reduce(uint[] b)
+	{
+		/*
+		 * If the modulus uses only one word then we must use
+		 * a special code path.
+		 */
+		if (mod.val.Length == 1) {
+			ReduceSmallMod(b);
+			return;
+		}
+
+		/*
+		 * Fast copy of words that do not incur modular
+		 * reduction.
+		 */
+		int aLen = mod.val.Length;
+		int bLen = b.Length;
+		int cLen = Math.Min(aLen - 1, bLen);
+		Array.Copy(b, bLen - cLen, val, 0, cLen);
+		for (int i = cLen; i < aLen; i ++) {
+			val[i] = 0;
+		}
+
+		/*
+		 * Inject extra words. We use the pre-computed
+		 * Rx[] values to do shifts.
+		 */
+		for (int j = bLen - cLen; j > 0;) {
+			/*
+			 * We can add power-of-2 words, but less
+			 * than the modulus size. Note that the modulus
+			 * uses at least two words, so this process works.
+			 */
+			int k;
+			for (k = 0;; k ++) {
+				int nk = 1 << (k + 1);
+				if (nk >= aLen || nk > j) {
+					break;
+				}
+			}
+			int num = 1 << k;
+			MontyMul(val, mod.Rx[k], tmp1, mod.val, mod.n0i);
+			j -= num;
+			Array.Copy(b, j, tmp2, 0, num);
+			for (int i = num; i < tmp2.Length; i ++) {
+				tmp2[i] = 0;
+			}
+			ModAdd(tmp1, tmp2, val, mod.val, 0);
+		}
+	}
+
+	/*
+	 * Modular reduction in case the modulus fits on a single
+	 * word.
+	 */
+	void ReduceSmallMod(uint[] b)
+	{
+		uint x = 0;
+		uint n = mod.val[0];
+		int nlen = mod.bitLen;
+		uint n0i = mod.n0i;
+		uint r2 = mod.R2[0];
+		for (int i = b.Length - 1; i >= 0; i --) {
+			/*
+			 * Multiply x by R (Montgomery multiplication by R^2).
+			 */
+			ulong z = (ulong)x * (ulong)r2;
+			uint u = ((uint)z * n0i) & 0x7FFFFFFF;
+			z += (ulong)u * (ulong)n;
+			x = (uint)(z >> 31);
+
+			/*
+			 * Ensure x fits on 31 bits (it may be up to twice
+			 * the modulus at that point). If x >= 2^31 then,
+			 * necessarily, x is greater than the modulus, and
+			 * the subtraction is sound; moreover, in that case,
+			 * subtracting the modulus brings back x to less
+			 * than the modulus, hence fitting on 31 bits.
+			 */
+			x -= (uint)((int)x >> 31) & n;
+
+			/*
+			 * Add the next word, then reduce. The addition
+			 * does not overflow since both operands fit on
+			 * 31 bits.
+			 *
+			 * Since the modulus could be much smaller than
+			 * 31 bits, we need a full remainder operation here.
+			 */
+			x += b[i];
+
+			/*
+			 * Constant-time modular reduction.
+			 * We first perform two subtraction of the
+			 * shifted modulus to ensure that the high bit
+			 * is cleared. This allows the loop to work
+			 * properly.
+			 */
+			x -= (uint)((int)x >> 31) & (n << (31 - nlen));
+			x -= (uint)((int)x >> 31) & (n << (31 - nlen));
+			for (int j = 31 - nlen; j >= 0; j --) {
+				x -= (n << j);
+				x += (uint)((int)x >> 31) & (n << j);
+			}
+		}
+		val[0] = x;
+	}
+
+	/*
+	 * Encode into bytes. Big-endian unsigned encoding is used, the
+	 * returned array having the minimal length to encode the modulus.
+	 */
+	public byte[] Encode()
+	{
+		return Encode(false);
+	}
+
+	/*
+	 * Encode into bytes. Big-endian encoding is used; if 'signed' is
+	 * true, then signed encoding is used: returned value will have a
+	 * leading bit set to 0. Returned array length is the minimal size
+	 * for encoding the modulus (with a sign bit if using signed
+	 * encoding).
+	 */
+	public byte[] Encode(bool signed)
+	{
+		int x = mod.bitLen;
+		if (signed) {
+			x ++;
+		}
+		byte[] buf = new byte[(x + 7) >> 3];
+		Encode(buf, 0, buf.Length);
+		return buf;
+	}
+
+	/*
+	 * Encode into bytes. The provided array is fully set; big-endian
+	 * encoding is used, and extra leading bytes of value 0 are added
+	 * if necessary. If the destination array is too small, then the
+	 * value is silently truncated.
+	 */
+	public void Encode(byte[] buf)
+	{
+		Encode(buf, 0, buf.Length);
+	}
+
+	/*
+	 * Encode into bytes. The provided array chunk is fully set;
+	 * big-endian encoding is used, and extra leading bytes of value
+	 * 0 are added if necessary. If the destination array is too
+	 * small, then the value is silently truncated.
+	 */
+	public void Encode(byte[] buf, int off, int len)
+	{
+		EncodeBE(val, buf, off, len);
+	}
+
+	/*
+	 * Get the least significant bit of the value (0 or 1).
+	 */
+	public uint GetLSB()
+	{
+		return val[0] & (uint)1;
+	}
+
+	/*
+	 * Add a small integer to this instance. The small integer
+	 * 'x' MUST be lower than 2^31 and MUST be lower than the modulus.
+	 */
+	public void Add(uint x)
+	{
+		tmp1[0] = x;
+		for (int i = 1; i < tmp1.Length; i ++) {
+			tmp1[i] = 0;
+		}
+		ModAdd(val, tmp1, val, mod.val, 0);
+	}
+
+	/*
+	 * Add another value to this instance. The operand 'b' may
+	 * be the same as this instance.
+	 */
+	public void Add(ModInt b)
+	{
+		if (mod != b.mod) {
+			throw new CryptoException("Not same modulus");
+		}
+		ModAdd(val, b.val, val, mod.val, 0);
+	}
+
+	/*
+	 * Subtract a small integer from this instance. The small integer
+	 * 'x' MUST be lower than 2^31 and MUST be lower than the modulus.
+	 */
+	public void Sub(uint x)
+	{
+		tmp1[0] = x;
+		for (int i = 1; i < tmp1.Length; i ++) {
+			tmp1[i] = 0;
+		}
+		ModSub(val, tmp1, val, mod.val);
+	}
+
+	/*
+	 * Subtract another value from this instance. The operand 'b'
+	 * may be the same as this instance.
+	 */
+	public void Sub(ModInt b)
+	{
+		if (mod != b.mod) {
+			throw new CryptoException("Not same modulus");
+		}
+		ModSub(val, b.val, val, mod.val);
+	}
+
+	/*
+	 * Negate this value.
+	 */
+	public void Negate()
+	{
+		ModSub(null, val, val, mod.val);
+	}
+
+	/*
+	 * Convert this instance to Montgomery representation.
+	 */
+	public void ToMonty()
+	{
+		MontyMul(val, mod.R2, tmp1, mod.val, mod.n0i);
+		Array.Copy(tmp1, 0, val, 0, val.Length);
+	}
+
+	/*
+	 * Convert this instance back from Montgomery representation to
+	 * normal representation.
+	 */
+	public void FromMonty()
+	{
+		tmp1[0] = 1;
+		for (int i = 1; i < tmp1.Length; i ++) {
+			tmp1[i] = 0;
+		}
+		MontyMul(val, tmp1, tmp2, mod.val, mod.n0i);
+		Array.Copy(tmp2, 0, val, 0, val.Length);
+	}
+
+	/*
+	 * Compute a Montgomery multiplication with the provided
+	 * value. The other operand may be this instance.
+	 */
+	public void MontyMul(ModInt b)
+	{
+		if (mod != b.mod) {
+			throw new CryptoException("Not same modulus");
+		}
+		MontyMul(val, b.val, tmp1, mod.val, mod.n0i);
+		Array.Copy(tmp1, 0, val, 0, val.Length);
+	}
+
+	/*
+	 * Montgomery-square this instance.
+	 */
+	public void MontySquare()
+	{
+		MontyMul(val, val, tmp1, mod.val, mod.n0i);
+		Array.Copy(tmp1, 0, val, 0, val.Length);
+	}
+
+	/*
+	 * Perform modular exponentiation. Exponent is in big-endian
+	 * unsigned encoding.
+	 */
+	public void Pow(byte[] exp)
+	{
+		Pow(exp, 0, exp.Length);
+	}
+
+	/*
+	 * Perform modular exponentiation. Exponent is in big-endian
+	 * unsigned encoding.
+	 */
+	public void Pow(byte[] exp, int off, int len)
+	{
+		MontyMul(val, mod.R2, tmp1, mod.val, mod.n0i);
+		val[0] = 1;
+		for (int i = 1; i < val.Length; i ++) {
+			val[i] = 0;
+		}
+		for (int i = 0; i < len; i ++) {
+			int x = exp[off + len - 1 - i];
+			for (int j = 0; j < 8; j ++) {
+				MontyMul(val, tmp1, tmp2, mod.val, mod.n0i);
+				uint ctl = (uint)-((x >> j) & 1);
+				for (int k = 0; k < val.Length; k ++) {
+					val[k] = (tmp2[k] & ctl)
+						| (val[k] & ~ctl);
+				}
+				MontyMul(tmp1, tmp1, tmp2, mod.val, mod.n0i);
+				Array.Copy(tmp2, 0, tmp1, 0, tmp2.Length);
+			}
+		}
+	}
+
+	/*
+	 * Compute modular inverse of this value. If this instance is
+	 * zero, then it remains equal to zero. If the modulus is not
+	 * prime, then this function computes wrong values.
+	 */
+	public void Invert()
+	{
+		Pow(mod.vm2);
+	}
+
+	/*
+	 * Compute the square root for this value. This method assumes
+	 * that the modulus is prime, greater than or equal to 7, and
+	 * equal to 3 modulo 4; if it is not, then the returned value
+	 * and the contents of this instance are indeterminate.
+	 *
+	 * Returned value is -1 if the value was indeed a square, 0
+	 * otherwise. In the latter case, array contents are the square
+	 * root of the opposite of the original value.
+	 */
+	public uint SqrtBlum()
+	{
+		/*
+		 * We suppose that p = 3 mod 4; we raise to the power
+		 * (p-3)/4, then do an extra multiplication to go to
+		 * power (p+1)/4.
+		 *
+		 * Since we know the modulus bit length, we can do
+		 * the exponentiation from the high bits downwards.
+		 */
+		ToMonty();
+		Array.Copy(val, 0, tmp1, 0, val.Length);
+		int k = (mod.bitLen - 2) / 31;
+		int j = mod.bitLen - 2 - k * 31;
+		uint ew = mod.val[k];
+		for (int i = mod.bitLen - 2; i >= 2; i --) {
+			uint ctl = ~(uint)-((int)(ew >> j) & 1);
+			MontyMul(tmp1, tmp1, tmp2, mod.val, mod.n0i);
+			MontyMul(val, tmp2, tmp1, mod.val, mod.n0i);
+			for (int m = 0; m < tmp1.Length; m ++) {
+				uint w = tmp1[m];
+				tmp1[m] = w ^ (ctl & (w ^ tmp2[m]));
+			}
+			if (-- j < 0) {
+				j = 30;
+				ew = mod.val[-- k];
+			}
+		}
+
+		/*
+		 * The extra multiplication. Square root is written in
+		 * tmp2 (in Montgomery representation).
+		 */
+		MontyMul(val, tmp1, tmp2, mod.val, mod.n0i);
+
+		/*
+		 * Square it back in tmp1, to see if it indeed yields
+		 * val.
+		 */
+		MontyMul(tmp2, tmp2, tmp1, mod.val, mod.n0i);
+		int z = 0;
+		for (int i = 0; i < val.Length; i ++) {
+			z |= (int)(val[i] ^ tmp1[i]);
+		}
+		uint good = ~(uint)((z | -z) >> 31);
+
+		/*
+		 * Convert back the result to normal representation.
+		 */
+		Array.Copy(tmp2, 0, val, 0, val.Length);
+		FromMonty();
+		return good;
+	}
+
+	/*
+	 * Conditionally swap this instance with the one provided in
+	 * parameter. The swap is performed if ctl is -1, not performed
+	 * if ctl is 0.
+	 */
+	public void CondSwap(ModInt b, uint ctl)
+	{
+		if (mod != b.mod) {
+			throw new CryptoException("Not same modulus");
+		}
+		for (int i = 0; i < val.Length; i ++) {
+			uint x = val[i];
+			uint y = b.val[i];
+			uint m = ctl & (x ^ y);
+			val[i] = x ^ m;
+			b.val[i] = y ^ m;
+		}
+	}
+
+	/*
+	 * Compare for equality this value with another. Comparison still
+	 * works if the two values use distinct moduli.
+	 */
+	public bool Eq(ModInt b)
+	{
+		return EqCT(b) != 0;
+	}
+
+	/*
+	 * Compare for equality this value with another. Comparison still
+	 * works if the two values use distinct moduli. Returned value is
+	 * -1 on equality, 0 otherwise.
+	 */
+	public uint EqCT(ModInt b)
+	{
+		uint z = 0;
+		if (b.val.Length > val.Length) {
+			for (int i = 0; i < val.Length; i ++) {
+				z |= val[i] ^ b.val[i];
+			}
+			for (int i = val.Length; i < b.val.Length; i ++) {
+				z |= b.val[i];
+			}
+		} else {
+			for (int i = 0; i < b.val.Length; i ++) {
+				z |= val[i] ^ b.val[i];
+			}
+			for (int i = b.val.Length; i < val.Length; i ++) {
+				z |= b.val[i];
+			}
+		}
+		int x = (int)z;
+		return ~(uint)((x | -x) >> 31);
+	}
+
+	/* ============================================================== */
+
+	/*
+	 * Decode value (unsigned big-endian) into dst[] (little-endian,
+	 * 31 bits per word). All words of dst[] are initialised.
+	 * Returned value is -1 on success, 0 if some non-zero source
+	 * bits had to be ignored.
+	 */
+	static uint DecodeBE(byte[] buf, int off, int len, uint[] dst)
+	{
+		int i = 0;
+		uint acc = 0;
+		int accLen = 0;
+		off += len;
+		while (i < dst.Length) {
+			uint b;
+			if (len > 0) {
+				b = buf[-- off];
+				len --;
+			} else {
+				b = 0;
+			}
+			acc |= (b << accLen);
+			accLen += 8;
+			if (accLen >= 31) {
+				dst[i ++] = acc & 0x7FFFFFFF;
+				accLen -= 31;
+				acc = b >> (8 - accLen);
+			}
+		}
+		while (len -- > 0) {
+			acc |= buf[-- off];
+		}
+		int x = (int)acc;
+		return ~(uint)((x | -x) >> 31);
+	}
+
+	/*
+	 * Encode an integer (array of words, little-endian, 31 bits per
+	 * word) into big-endian encoding, with the provided length (in
+	 * bytes).
+	 */
+	static byte[] EncodeBE(uint[] x, int len)
+	{
+		byte[] val = new byte[len];
+		EncodeBE(x, val, 0, len);
+		return val;
+	}
+
+	/*
+	 * Encode an integer (array of words, little-endian, 31 bits per
+	 * word) into big-endian encoding, with the provided length (in
+	 * bytes).
+	 */
+	static void EncodeBE(uint[] x, byte[] val)
+	{
+		EncodeBE(x, val, 0, val.Length);
+	}
+
+	/*
+	 * Encode an integer (array of words, little-endian, 31 bits per
+	 * word) into big-endian encoding, with the provided length (in
+	 * bytes).
+	 */
+	static void EncodeBE(uint[] x, byte[] val, int off, int len)
+	{
+		uint acc = 0;
+		int accLen = 0;
+		int j = 0;
+		for (int i = len - 1; i >= 0; i --) {
+			uint b;
+			if (accLen < 8) {
+				uint z = (j < x.Length) ? x[j ++] : 0;
+				b = acc | (z << accLen);
+				acc = z >> (8 - accLen);
+				accLen += 23;
+			} else {
+				b = acc;
+				accLen -= 8;
+				acc >>= 8;
+			}
+			val[off + i] = (byte)b;
+		}
+	}
+
+	/*
+	 * Subtract b from a; the carry is returned (-1 if carry, 0
+	 * otherwise). The operation is done only if ctl is -1; if
+	 * ctl is 0, then a[] is unmodified, but the carry is still
+	 * computed and returned.
+	 *
+	 * The two operand arrays MUST have the same size.
+	 */
+	static uint Sub(uint[] a, uint[] b, uint ctl)
+	{
+		int n = a.Length;
+		int cc = 0;
+		ctl >>= 1;
+		for (int i = 0; i < n; i ++) {
+			uint aw = a[i];
+			uint bw = b[i];
+			uint cw = (uint)cc + aw - bw;
+			cc = (int)cw >> 31;
+			a[i] = (aw & ~ctl) | (cw & ctl);
+		}
+		return (uint)cc;
+	}
+
+	/*
+	 * Left-shift value by one bit; the extra bit (carry) is
+	 * return as -1 or 0.
+	 */
+	static uint LShift(uint[] a)
+	{
+		int n = a.Length;
+		uint cc = 0;
+		for (int i = 0; i < n; i ++) {
+			uint aw = a[i];
+			a[i] = (cc | (aw << 1)) & 0x7FFFFFFF;
+			cc = aw >> 30;
+		}
+		return (uint)-(int)cc;
+	}
+
+	/*
+	 * Modular left-shift value by one bit. Value and modulus MUST
+	 * have the same length. Value MUST be lower than modulus.
+	 */
+	static void ModMul2(uint[] a, uint[] mod)
+	{
+		int n = a.Length;
+
+		/*
+		 * First pass: compute 2*a-mod, but don't keep the
+		 * result, only the final carry (0 or -1).
+		 */
+		uint cc1 = 0;
+		int cc2 = 0;
+		for (int i = 0; i < n; i ++) {
+			uint aw = a[i];
+			uint aws = ((aw << 1) | cc1) & 0x7FFFFFFF;
+			cc1 = aw >> 30;
+			uint z = aws - mod[i] + (uint)cc2;
+			cc2 = (int)z >> 31;
+		}
+		cc2 += (int)cc1;
+
+		/*
+		 * If cc2 is 0, then the subtraction yields no carry and
+		 * must be done. Otherwise, cc2 is -1, and the subtraction
+		 * must not be done.
+		 */
+		uint ctl = ~(uint)cc2;
+		cc1 = 0;
+		cc2 = 0;
+		for (int i = 0; i < n; i ++) {
+			uint aw = a[i];
+			uint aws = ((aw << 1) | cc1) & 0x7FFFFFFF;
+			cc1 = aw >> 30;
+			uint z = aws - (mod[i] & ctl) + (uint)cc2;
+			cc2 = (int)z >> 31;
+			a[i] = z & 0x7FFFFFFF;
+		}
+	}
+
+	/*
+	 * Modular addition.
+	 *
+	 * If 'hi' is zero, then this computes a+b-n if a+b >= n,
+	 * a+b otherwise.
+	 *
+	 * If 'hi' is non-zero, then this computes 2^k+a+b-n, where
+	 * k = n.Length*31.
+	 *
+	 * Result is written in d[]. The same array may be used in
+	 * several operands.
+	 */
+	static void ModAdd(uint[] a, uint[] b, uint[] d, uint[] n, uint hi)
+	{
+		/*
+		 * Set ctl to -1 if hi is non-zero, 0 otherwise.
+		 * 'ctl' computes whether the subtraction with n[]
+		 * is needed in the second pass.
+		 */
+		int x = (int)hi;
+		uint ctl = (uint)((x | -x) >> 31);
+
+		for (int pass = 0; pass < 2; pass ++) {
+			/*
+			 * cc1 is the carry for a+b (0 or 1)
+			 *
+			 * cc2 is the carry for the modulus
+			 * subtraction (0 or -1)
+			 */
+			uint cc1 = 0;
+			int cc2 = 0;
+			for (int i = 0; i < n.Length; i ++) {
+				uint aw = a[i];
+				uint bw = b[i];
+				uint nw = n[i];
+				uint sw = aw + bw + cc1;
+				cc1 = sw >> 31;
+				sw &= 0x7FFFFFFF;
+				uint dw = sw - nw + (uint)cc2;
+				cc2 = (int)dw >> 31;
+				if (pass == 1) {
+					dw &= 0x7FFFFFFF;
+					d[i] = (dw & ctl) | (sw & ~ctl);
+				}
+			}
+
+			/*
+			 * Compute aggregate subtraction carry. This should
+			 * not be 1 if the operands are correct (it would
+			 * mean that a+b-n overflows, so both a and b are
+			 * larger than n). If it is 0, then a+b-n >= 0,
+			 * so the subtraction must be done; otherwise, the
+			 * aggregate carry will be -1, and the subtraction
+			 * should not be done (unless forced by a non-zero
+			 * 'hi' value).
+			 */
+			cc2 += (int)cc1;
+			ctl |= ~(uint)(cc2 >> 31);
+		}
+	}
+
+	/*
+	 * Modular subtraction.
+	 *
+	 * This computes a-b, then adds n if the result is negative.
+	 * If a is null, then it is assumed to be all-zeros.
+	 *
+	 * Result is written in d[]. The same array may be used in
+	 * several operands.
+	 */
+	static void ModSub(uint[] a, uint[] b, uint[] d, uint[] n)
+	{
+		uint ctl = 0;
+		for (int pass = 0; pass < 2; pass ++) {
+			/*
+			 * cc1 = carry for a-b (0 or -1)
+			 * cc2 = carry for modulus addition (0 or 1)
+			 */
+			int cc1 = 0;
+			uint cc2 = 0;
+			for (int i = 0; i < n.Length; i ++) {
+				uint aw = (a == null) ? 0 : a[i];
+				uint bw = b[i];
+				uint nw = n[i];
+				uint sw = aw - bw + (uint)cc1;
+				cc1 = (int)sw >> 31;
+				sw &= 0x7FFFFFFF;
+				uint dw = sw + nw + cc2;
+				cc2 = dw >> 31;
+				if (pass == 1) {
+					dw &= 0x7FFFFFFF;
+					d[i] = (dw & ctl) | (sw & ~ctl);
+				}
+			}
+
+			/*
+			 * Modulus addition must be done if and only if
+			 * a-b had a final carry.
+			 */
+			ctl = (uint)cc1;
+		}
+	}
+
+	/*
+	 * Compute Montgomery multiplication of a[] by b[], result in
+	 * d[], with modulus n[]. All arrays must have the same length.
+	 * d[] must be distinct from a[], b[] and n[]. Modulus must be
+	 * odd. n0i must be such that n[0]*n0i = -1 mod 2^31. Values a[]
+	 * and b[] must be lower than n[] (so, in particular, a[] and
+	 * b[] cannot be the same array as n[]).
+	 */
+	static void MontyMul(uint[] a, uint[] b, uint[] d, uint[] n, uint n0i)
+	{
+		int len = n.Length;
+		for (int i = 0; i < len; i ++) {
+			d[i] = 0;
+		}
+		ulong dh = 0;
+		for (int i = 0; i < len; i ++) {
+			uint ai = a[i];
+			uint u = ((d[0] + ai * b[0]) * n0i) & 0x7FFFFFFF;
+			ulong cc = 0;
+			for (int j = 0; j < len; j ++) {
+				ulong z = (ulong)d[j]
+					+ (ulong)ai * (ulong)b[j]
+					+ (ulong)u * (ulong)n[j] + cc;
+				cc = z >> 31;
+				if (j > 0) {
+					d[j - 1] = (uint)z & 0x7FFFFFFF;
+				} else {
+					// DEBUG
+					if (((uint)z & 0x7FFFFFFF) != 0) {
+						throw new Exception("BAD!");
+					}
+				}
+			}
+			dh += cc;
+			d[len - 1] = (uint)dh & 0x7FFFFFFF;
+			dh >>= 31;
+		}
+		int x = (int)dh;
+		uint ctl = (uint)((x | -x) >> 31);
+		Sub(d, n, ctl | ~Sub(d, n, 0));
+	}
+}
+
+}
diff --git a/Crypto/MutableECPoint.cs b/Crypto/MutableECPoint.cs
new file mode 100644
index 0000000..ede06df
--- /dev/null
+++ b/Crypto/MutableECPoint.cs
@@ -0,0 +1,252 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * A MutableECPoint instance contains an elliptic curve point, in a
+ * given curve. It may be modified to contain another point, but not
+ * on another curve.
+ *
+ * Constant-time guarantees: IsInfinityCT, DoubleCT(), NegCT() and
+ * AddCT() are constant-time with regards to the represented curve
+ * point. Execution time may vary depending on the sequence of calls,
+ * but not on the point data. In particular, points may be internally
+ * "normalized" or not, and operations involving normalized points can
+ * be faster; however, normalization happens only upon an explicit call.
+ * The normalization process itself (Normalize()) is constant-time.
+ */
+
+internal abstract class MutableECPoint {
+
+	internal MutableECPoint()
+	{
+	}
+
+	internal abstract ECCurve Curve {
+		get;
+	}
+
+	/*
+	 * Test whether this point is the point at infinity.
+	 */
+	internal bool IsInfinity {
+		get {
+			return IsInfinityCT != 0;
+		}
+	}
+
+	/*
+	 * Test whether this point is the point at infinity (returns
+	 * 0xFFFFFFFF or 0x00000000).
+	 */
+	internal abstract uint IsInfinityCT {
+		get;
+	}
+
+	/*
+	 * Normalize this instance. What this entails depends on the
+	 * curve type, but it will typically means computing affine
+	 * coordinates in case this instance was using some sort of
+	 * projective system.
+	 */
+	internal abstract void Normalize();
+
+	/*
+	 * Encode this point into some bytes. If "compressed" is true,
+	 * then a compressed format will be used.
+	 *
+	 * This call may entail normalization. If the point is not the
+	 * infinity point, then this method is constant-time.
+	 */
+	internal abstract byte[] Encode(bool compressed);
+
+	/*
+	 * Encode this point into some bytes. If "compressed is true,
+	 * then a compressed format will be used. The destination array
+	 * must have the proper length for the requested point format.
+	 *
+	 * This call may entail normalization. If the point is invalid
+	 * or is the point at infinity, then the returned value is 0
+	 * and what gets written in the array is indeterminate. Otherwise,
+	 * the encoded point is written and -1 is returned. Either way,
+	 * this call is constant-time.
+	 */
+	internal abstract uint Encode(byte[] dst, bool compressed);
+
+	/*
+	 * Set this point by decoding the provided value. An invalid
+	 * encoding sets this point to 0 (infinity) and triggers an
+	 * exception.
+	 */
+	internal void Decode(byte[] enc)
+	{
+		if (DecodeCT(enc) == 0) {
+			throw new CryptoException("Invalid encoded point");
+		}
+	}
+
+	/*
+	 * Set this point by decoding the provided value. This is
+	 * constant-time (up to the encoded point length). Returned
+	 * value is 0xFFFFFFFF if the encoded point was valid,
+	 * 0x00000000 otherwise. If the decoding failed, then this
+	 * value is set to 0 (infinity).
+	 */
+	internal abstract uint DecodeCT(byte[] enc);
+
+	/*
+	 * Get the X coordinate for this point. This implies
+	 * normalization. If the point is the point at infinity,
+	 * then the returned array contains the encoding of 0.
+	 * This is constant-time.
+	 */
+	internal abstract byte[] X {
+		get;
+	}
+
+	/*
+	 * Get the Y coordinate for this point. This implies
+	 * normalization. If the point is the point at infinity,
+	 * then the returned array contains the encoding of 0.
+	 * This is constant-time.
+	 */
+	internal abstract byte[] Y {
+		get;
+	}
+
+	/*
+	 * Create a new instance that starts with the same contents as
+	 * this point.
+	 */
+	internal abstract MutableECPoint Dup();
+
+	/*
+	 * Set this instance to the point at infinity.
+	 */
+	internal abstract void SetZero();
+
+	/*
+	 * Set this instance to the same contents as the provided point.
+	 * The operand Q must be part of the same curve.
+	 */
+	internal abstract void Set(MutableECPoint Q);
+
+	/*
+	 * Set this instance to the same contents as the provided point,
+	 * but only if ctl == 0xFFFFFFFFF. If ctl == 0x00000000, then
+	 * this instance is unmodified. The operand Q must be part of
+	 * the same curve.
+	 */
+	internal abstract void Set(MutableECPoint Q, uint ctl);
+
+	/*
+	 * Set this instance to the same contents as point P1 if
+	 * ctl == 0xFFFFFFFF, or point P2 if ctl == 0x00000000.
+	 * Both operands must use the same curve as this instance.
+	 */
+	internal abstract void SetMux(uint ctl,
+		MutableECPoint P1, MutableECPoint P2);
+
+	/*
+	 * DoubleCT() is constant-time. It works for all points
+	 * (including points of order 2 and the infinity point).
+	 */
+	internal abstract void DoubleCT();
+
+	/*
+	 * AddCT() computes P+Q (P is this instance, Q is the operand).
+	 * It may assume that P != Q. If P = Q and the method could not
+	 * compute the correct result, then it shall set this instance to
+	 * 0 (infinity) and return 0x00000000. In all other cases, it must
+	 * compute the correct point and return 0xFFFFFFFF. In particular,
+	 * it should properly handle cases where P = 0 or Q = 0. This
+	 * function is allowed to handle doubling cases as well, if it
+	 * can.
+	 *
+	 * This method may be more efficient if the operand is
+	 * normalized. Execution time and memory access may depend on
+	 * whether this instance or the other operand is normalized,
+	 * but not on the actual point values (including if the points
+	 * do not fulfill the properties above).
+	 */
+	internal abstract uint AddCT(MutableECPoint Q);
+
+	/*
+	 * Negate this point. It also works on the point at infinity,
+	 * and it is constant-time.
+	 */
+	internal abstract void NegCT();
+
+	/*
+	 * Multiply this point by the provided integer (unsigned
+	 * big-endian representation). This is constant-time. This
+	 * method assumes that:
+	 * -- the point on which we are operating is part of the curve
+	 *    defined subgroup;
+	 * -- the defined subgroup has a prime order which is no less
+	 *    than 17;
+	 * -- the point is not the point at infinity;
+	 * -- the multiplier operand is no more than the subgroup order.
+	 * If these conditions are met, then the resulting point will
+	 * be the proper element of the defined subgroup (it will be
+	 * the point at infinity only if the multiplier is 0 or is
+	 * equal to the subgroup order). If they are NOT met, then the
+	 * resulting point is undefined (but will still be part of the
+	 * curve).
+	 *
+	 * This method is constant-time.
+	 *
+	 * Returned value is 0xFFFFFFFF if none of the internal
+	 * operations reached a problematic state (i.e. that we tried to
+	 * perform an addition and the two operands turned out to be
+	 * equal to each other). If the conditions above are met, then
+	 * this is always the case. If a problematic state was reached,
+	 * then the returned value is 0x00000000. Callers MUST be very
+	 * cautious about using that reported error state, since it is
+	 * not guaranteed that all invalid points would be reported as
+	 * such. There thus is potential for leakage of secret data.
+	 */
+	internal abstract uint MulSpecCT(byte[] n);
+
+	/*
+	 * Compare this point to another. This method throws an
+	 * exception if the provided point is not on the same curve as
+	 * this instance. It otherwise returns 0xFFFFFFFF if both points
+	 * are equal, 0x00000000 otherwise. This method is constant-time
+	 * (its execution time may depend on whether this and/or the
+	 * other point is normalized or not, but not on the actual
+	 * values).
+	 */
+	internal abstract uint EqCT(MutableECPoint Q);
+
+	internal bool Eq(MutableECPoint Q)
+	{
+		return EqCT(Q) != 0;
+	}
+}
+
+}
diff --git a/Crypto/MutableECPointCurve25519.cs b/Crypto/MutableECPointCurve25519.cs
new file mode 100644
index 0000000..b3fdf7a
--- /dev/null
+++ b/Crypto/MutableECPointCurve25519.cs
@@ -0,0 +1,286 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * An implementation of MutableECPoint for Curve25519.
+ * This is a partial implementation that only supports multiplication
+ * by a scalar, not general addition.
+ */
+
+internal class MutableECPointCurve25519 : MutableECPoint {
+
+	ModInt x;
+	ModInt x1, x2, z2, x3, z3;
+	ModInt a, aa, b, bb, c, d, e;
+	byte[] u;
+
+	/*
+	 * Create a new instance. It is initialized to the point at
+	 * infinity (represented with a 0).
+	 */
+	internal MutableECPointCurve25519()
+	{
+		ECCurve25519 ccc = (ECCurve25519)EC.Curve25519;
+		x = ccc.mp.Dup();
+		x1 = ccc.mp.Dup();
+		x2 = ccc.mp.Dup();
+		z2 = ccc.mp.Dup();
+		x3 = ccc.mp.Dup();
+		z3 = ccc.mp.Dup();
+		a = ccc.mp.Dup();
+		aa = ccc.mp.Dup();
+		b = ccc.mp.Dup();
+		bb = ccc.mp.Dup();
+		c = ccc.mp.Dup();
+		d = ccc.mp.Dup();
+		e = ccc.mp.Dup();
+		u = new byte[32];
+	}
+
+	internal override ECCurve Curve {
+		get {
+			return EC.Curve25519;
+		}
+	}
+
+	internal override uint IsInfinityCT {
+		get {
+			return 0;
+		}
+	}
+
+	internal override void Normalize()
+	{
+	}
+
+	internal override byte[] Encode(bool compressed)
+	{
+		byte[] r = new byte[32];
+		Encode(r, false);
+		return r;
+	}
+
+	internal override uint Encode(byte[] dst, bool compressed)
+	{
+		if (dst.Length != 32) {
+			throw new CryptoException("invalid output length");
+		}
+		x.Encode(u, 0, 32);
+		for (int i = 0; i < 32; i ++) {
+			dst[i] = u[31 - i];
+		}
+		return 0xFFFFFFFF;
+	}
+
+	internal override uint DecodeCT(byte[] enc)
+	{
+		if (enc.Length != 32) {
+			return 0;
+		}
+		for (int i = 0; i < 32; i ++) {
+			u[i] = enc[31 - i];
+		}
+		u[0] &= 0x7F;
+		x.DecodeReduce(u);
+		return 0xFFFFFFFF;
+	}
+
+	internal override byte[] X {
+		get {
+			return x.Encode();
+		}
+	}
+
+	internal override byte[] Y {
+		get {
+			throw new CryptoException(
+				"Not implemented for Curve25519");
+		}
+	}
+
+	internal override MutableECPoint Dup()
+	{
+		MutableECPointCurve25519 Q = new MutableECPointCurve25519();
+		Q.Set(this);
+		return Q;
+	}
+
+	internal void Set(byte[] X, byte[] Y, bool check)
+	{
+		throw new CryptoException("Not implemented for Curve25519");
+	}
+
+	internal void Set(ModInt X, ModInt Y, bool check)
+	{
+		throw new CryptoException("Not implemented for Curve25519");
+	}
+
+	internal override void SetZero()
+	{
+		throw new CryptoException("Not implemented for Curve25519");
+	}
+
+	internal override void Set(MutableECPoint Q)
+	{
+		MutableECPointCurve25519 R = SameCurve(Q);
+		x.Set(R.x);
+	}
+
+	internal override void Set(MutableECPoint Q, uint ctl)
+	{
+		MutableECPointCurve25519 R = SameCurve(Q);
+		x.CondCopy(R.x, ctl);
+	}
+
+	internal override void SetMux(uint ctl,
+		MutableECPoint P1, MutableECPoint P2)
+	{
+		SetMuxInner(ctl, SameCurve(P1), SameCurve(P2));
+	}
+
+	void SetMuxInner(uint ctl,
+		MutableECPointCurve25519 P1, MutableECPointCurve25519 P2)
+	{
+		x.CopyMux(ctl, P1.x, P2.x);
+	}
+
+	internal override void DoubleCT()
+	{
+		throw new CryptoException("Not implemented for Curve25519");
+	}
+
+	internal override uint AddCT(MutableECPoint Q)
+	{
+		throw new CryptoException("Not implemented for Curve25519");
+	}
+
+	internal override void NegCT()
+	{
+		throw new CryptoException("Not implemented for Curve25519");
+	}
+
+	internal override uint MulSpecCT(byte[] n)
+	{
+		/*
+		 * Copy scalar into a temporary array (u[]) for
+		 * normalisation to 32 bytes and clamping.
+		 */
+		if (n.Length > 32) {
+			return 0;
+		}
+		Array.Copy(n, 0, u, 32 - n.Length, n.Length);
+		for (int i = 0; i < 32 - n.Length; i ++) {
+			u[i] = 0;
+		}
+		u[31] &= 0xF8;
+		u[0] &= 0x7F;
+		u[0] |= 0x40;
+
+		x1.Set(x);
+		x1.ToMonty();
+		x2.SetMonty(0xFFFFFFFF);
+		z2.Set(0);
+		x3.Set(x1);
+		z3.Set(x2);
+		uint swap = 0;
+		ModInt ma24 = ((ECCurve25519)EC.Curve25519).ma24;
+
+		for (int t = 254; t >= 0; t --) {
+			uint kt = (uint)-((u[31 - (t >> 3)] >> (t & 7)) & 1);
+			swap ^= kt;
+			x2.CondSwap(x3, swap);
+			z2.CondSwap(z3, swap);
+			swap = kt;
+
+			a.Set(x2);
+			a.Add(z2);
+			aa.Set(a);
+			aa.MontySquare();
+			b.Set(x2);
+			b.Sub(z2);
+			bb.Set(b);
+			bb.MontySquare();
+			e.Set(aa);
+			e.Sub(bb);
+			c.Set(x3);
+			c.Add(z3);
+			d.Set(x3);
+			d.Sub(z3);
+			d.MontyMul(a);
+			c.MontyMul(b);
+			x3.Set(d);
+			x3.Add(c);
+			x3.MontySquare();
+			z3.Set(d);
+			z3.Sub(c);
+			z3.MontySquare();
+			z3.MontyMul(x1);
+			x2.Set(aa);
+			x2.MontyMul(bb);
+			z2.Set(e);
+			z2.MontyMul(ma24);
+			z2.Add(aa);
+			z2.MontyMul(e);
+		}
+		x2.CondSwap(x3, swap);
+		z2.CondSwap(z3, swap);
+
+		/*
+		 * We need to restore z2 to normal representation before
+		 * inversion. Then the final Montgomery multiplication
+		 * will cancel out with x2, which is still in Montgomery
+		 * representation.
+		 */
+		z2.FromMonty();
+		z2.Invert();
+		x2.MontyMul(z2);
+
+		/*
+		 * x2 now contains the result.
+		 */
+		x.Set(x2);
+		return 0xFFFFFFFF;
+	}
+
+	internal override uint EqCT(MutableECPoint Q)
+	{
+		MutableECPointCurve25519 R = SameCurve(Q);
+		return x.EqCT(R.x);
+	}
+
+	MutableECPointCurve25519 SameCurve(MutableECPoint Q)
+	{
+		MutableECPointCurve25519 R = Q as MutableECPointCurve25519;
+		if (R == null) {
+			throw new CryptoException("Mixed curves");
+		}
+		return R;
+	}
+}
+
+}
diff --git a/Crypto/MutableECPointPrime.cs b/Crypto/MutableECPointPrime.cs
new file mode 100644
index 0000000..fa4b12c
--- /dev/null
+++ b/Crypto/MutableECPointPrime.cs
@@ -0,0 +1,907 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * An implementation of MutableECPoint for curves in a prime field.
+ * Jacobian coordinates are used.
+ */
+
+internal class MutableECPointPrime : MutableECPoint {
+
+	/*
+	 * Internal representation:
+	 *    x = X / (Z^2)
+	 *    y = Y / (Z^3)
+	 * For the point at infinity, Z == 0.
+	 *
+	 * When 'affine' is true (0xFFFFFFFF), then mx and my are in
+	 * normal representation, and mz is either 0 (point at infinity)
+	 * or 1 (otherwise). When 'affine' is false (0x00000000), then
+	 * mx, my and mz are in Montgomery representation.
+	 *
+	 * Note that in affine coordinates, the point at infinity admits
+	 * several equivalent representations. In non-affine
+	 * coordinates, all points have several equivalent
+	 * representations.
+	 */
+
+	ECCurvePrime curve;
+	ModInt mx, my, mz;
+	uint affine;
+	ModInt mt1, mt2, mt3, mt4, mt5;
+	ModInt ms1, ms2, ms3, ms4, ms5, ms6;
+
+	/*
+	 * Create a new instance. It is initialized to the point at
+	 * infinity.
+	 */
+	internal MutableECPointPrime(ECCurvePrime curve)
+	{
+		this.curve = curve;
+		mx = curve.mp.Dup();
+		my = curve.mp.Dup();
+		mz = curve.mp.Dup();
+		mt1 = curve.mp.Dup();
+		mt2 = curve.mp.Dup();
+		mt3 = curve.mp.Dup();
+		mt4 = curve.mp.Dup();
+		mt5 = curve.mp.Dup();
+		ms1 = curve.mp.Dup();
+		ms2 = curve.mp.Dup();
+		ms3 = curve.mp.Dup();
+		ms4 = curve.mp.Dup();
+		ms5 = curve.mp.Dup();
+		ms6 = curve.mp.Dup();
+		affine = 0xFFFFFFFF;
+	}
+
+	internal override ECCurve Curve {
+		get {
+			return curve;
+		}
+	}
+
+	internal override uint IsInfinityCT {
+		get {
+			return mz.IsZeroCT;
+		}
+	}
+
+	internal override void Normalize()
+	{
+		ToAffine();
+	}
+
+	internal override byte[] Encode(bool compressed)
+	{
+		ToAffine();
+		if (IsInfinity) {
+			return new byte[1];
+		}
+		if (compressed) {
+			byte[] enc = new byte[curve.EncodedLengthCompressed];
+			enc[0] = (byte)(0x02 + my.GetLSB());
+			mx.Encode(enc, 1, enc.Length - 1);
+			return enc;
+		} else {
+			byte[] enc = new byte[curve.EncodedLength];
+			int flen = (enc.Length - 1) >> 1;
+			enc[0] = 0x04;
+			mx.Encode(enc, 1, flen);
+			my.Encode(enc, 1 + flen, flen);
+			return enc;
+		}
+	}
+
+	internal override uint Encode(byte[] dst, bool compressed)
+	{
+		ToAffine();
+		if (compressed) {
+			int len = curve.EncodedLengthCompressed;
+			if (dst.Length != len) {
+				throw new CryptoException(
+					"invalid output length");
+			}
+			dst[0] = (byte)(0x02 + my.GetLSB());
+			mx.Encode(dst, 1, len - 1);
+		} else {
+			int len = curve.EncodedLength;
+			if (dst.Length != len) {
+				throw new CryptoException(
+					"invalid output length");
+			}
+			int flen = (len - 1) >> 1;
+			dst[0] = 0x04;
+			mx.Encode(dst, 1, flen);
+			my.Encode(dst, 1 + flen, flen);
+		}
+		return ~IsInfinityCT;
+	}
+
+	internal override uint DecodeCT(byte[] enc)
+	{
+		/*
+		 * Format (specified in IEEE P1363, annex E):
+		 *
+		 *   0x00             point at infinity
+		 *   0x02+b <X>       compressed, b = lsb of Y
+		 *   0x04 <X> <Y>     uncompressed
+		 *   0x06+b <X> <Y>   uncompressed, b = lsb of Y
+		 *
+		 * Coordinates X and Y are in unsigned big-endian
+		 * notation with exactly the length of the modulus.
+		 *
+		 * We want constant-time decoding, up to the encoded
+		 * length. This means that the four following situations
+		 * can be differentiated:
+		 * -- Point is zero (length = 1)
+		 * -- Point is compressed (length = 1 + flen)
+		 * -- Point is uncompressed or hybrid (length = 1 + 2*flen)
+		 * -- Length is neither 1, 1+flen or 1+2*flen.
+		 */
+
+		int flen = curve.flen;
+		uint good = 0xFFFFFFFF;
+		if (enc.Length == 1) {
+			/*
+			 * 1-byte encoding is point at infinity; the
+			 * byte shall have value 0.
+			 */
+			int z = enc[0];
+			good &= ~(uint)((z | -z) >> 31);
+			SetZero();
+		} else if (enc.Length == 1 + flen) {
+			/*
+			 * Compressed encoding. Leading byte is 0x02 or
+			 * 0x03.
+			 */
+			int z = (enc[0] & 0xFE) - 0x02;
+			good &= ~(uint)((z | -z) >> 31);
+			uint lsbValue = (uint)(enc[0] & 1);
+			good &= mx.Decode(enc, 1, flen);
+			RebuildY2();
+			if (curve.pMod4 == 3) {
+				good &= my.SqrtBlum();
+			} else {
+				/*
+				 * Square roots modulo a non-Blum prime
+				 * are a bit more complex. We do not
+				 * support them yet (TODO).
+				 */
+				good = 0x00000000;
+			}
+
+			/*
+			 * Adjust Y depending on LSB.
+			 */
+			mt1.Set(my);
+			mt1.Negate();
+			uint dn = (uint)-(int)(my.GetLSB() ^ lsbValue);
+			my.CondCopy(mt1, dn);
+
+			/*
+			 * A corner case: LSB adjustment works only if
+			 * Y != 0. If Y is 0 and requested LSB is 1,
+			 * then the decoding fails. Note that this case
+			 * cannot happen with usual prime curves, because
+			 * they have a prime order, implying that there is
+			 * no valid point such that Y = 0 (that would be
+			 * a point of order 2).
+			 */
+			good &= ~(uint)-(int)(my.GetLSB() ^ lsbValue);
+
+			mz.Set(1);
+		} else if (enc.Length == 1 + (flen << 1)) {
+			/*
+			 * Uncompressed or hybrid. Leading byte is either
+			 * 0x04, 0x06 or 0x07. We verify that the X and
+			 * Y coordinates fulfill the curve equation.
+			 */
+			int fb = enc[0];
+			int z = (fb & 0xFC) - 0x04;
+			good &= ~(uint)((z | -z) >> 31);
+			z = fb - 0x05;
+			good &= (uint)((z | -z) >> 31);
+			good &= mx.Decode(enc, 1, flen);
+			RebuildY2();
+			mt1.Set(my);
+			mt1.FromMonty();
+			good &= my.Decode(enc, 1 + flen, flen);
+			mt2.Set(my);
+			mt2.MontySquare();
+			good &= mt1.EqCT(mt2);
+
+			/*
+			 * We must check the LSB for hybrid encoding.
+			 * The check fails if the encoding is marked as
+			 * hybrid AND the LSB does not match.
+			 */
+			int lm = (fb >> 1) & ((int)my.GetLSB() ^ fb) & 1;
+			good &= ~(uint)-lm;
+
+			mz.Set(1);
+		} else {
+			good = 0x00000000;
+		}
+
+		/*
+		 * If decoding failed, then we force the value to 0.
+		 * Otherwise, we got a value. Either way, this uses
+		 * affine coordinates.
+		 */
+		mx.CondCopy(curve.mp, ~good);
+		my.CondCopy(curve.mp, ~good);
+		mz.CondCopy(curve.mp, ~good);
+		affine = 0xFFFFFFFF;
+		return good;
+	}
+
+	internal override byte[] X {
+		get {
+			ToAffine();
+			return mx.Encode();
+		}
+	}
+
+	internal override byte[] Y {
+		get {
+			ToAffine();
+			return my.Encode();
+		}
+	}
+
+	internal override MutableECPoint Dup()
+	{
+		MutableECPointPrime Q = new MutableECPointPrime(curve);
+		Q.Set(this);
+		return Q;
+	}
+
+	internal void Set(byte[] X, byte[] Y, bool check)
+	{
+		mx.Decode(X);
+		my.Decode(Y);
+		mz.Set(1);
+		affine = 0xFFFFFFFF;
+		if (check) {
+			CheckEquation();
+		}
+	}
+
+	internal void Set(ModInt X, ModInt Y, bool check)
+	{
+		mx.Set(X);
+		my.Set(Y);
+		mz.Set(1);
+		affine = 0xFFFFFFFF;
+		if (check) {
+			CheckEquation();
+		}
+	}
+
+	void CheckEquation()
+	{
+		curve.RebuildY2(mx, mt1, mt2);
+		mt2.Set(my);
+		mt2.ToMonty();
+		mt2.MontyMul(my);
+		if (!mt1.Eq(mt2)) {
+			throw new CryptoException(
+				"Point is not on the curve");
+		}
+	}
+
+	internal override void SetZero()
+	{
+		mx.Set(0);
+		my.Set(0);
+		mz.Set(0);
+		affine = 0xFFFFFFFF;
+	}
+
+	internal override void Set(MutableECPoint Q)
+	{
+		MutableECPointPrime R = SameCurve(Q);
+		mx.Set(R.mx);
+		my.Set(R.my);
+		mz.Set(R.mz);
+		affine = R.affine;
+	}
+
+	internal override void Set(MutableECPoint Q, uint ctl)
+	{
+		MutableECPointPrime R = SameCurve(Q);
+		mx.CondCopy(R.mx, ctl);
+		my.CondCopy(R.my, ctl);
+		mz.CondCopy(R.mz, ctl);
+		affine ^= ctl & (affine ^ R.affine);
+	}
+
+	internal override void SetMux(uint ctl,
+		MutableECPoint P1, MutableECPoint P2)
+	{
+		SetMuxInner(ctl, SameCurve(P1), SameCurve(P2));
+	}
+
+	void SetMuxInner(uint ctl,
+		MutableECPointPrime P1, MutableECPointPrime P2)
+	{
+		mx.CopyMux(ctl, P1.mx, P2.mx);
+		my.CopyMux(ctl, P1.my, P2.my);
+		mz.CopyMux(ctl, P1.mz, P2.mz);
+		affine = P2.affine ^ (ctl & (P1.affine ^ P2.affine));
+	}
+
+	internal override void DoubleCT()
+	{
+		ToJacobian();
+
+		/*
+		 * Formulas are:
+		 *   S = 4*X*Y^2
+		 *   M = 3*X^2 + a*Z^4
+		 *   X' = M^2 - 2*S
+		 *   Y' = M*(S - X') - 8*Y^4
+		 *   Z' = 2*Y*Z
+		 *
+		 * These formulas also happen to work properly (with our
+		 * chosen representation) when the source point has
+		 * order 2 (Y = 0 implies Z' = 0) and when the source
+		 * point is already the point at infinity (Z = 0 implies
+		 * Z' = 0).
+		 *
+		 * When a = -3, the value of M can be computed with the
+		 * more efficient formula:
+		 *   M = 3*(X+Z^2)*(X-Z^2)
+		 */
+
+		/*
+		 * Compute M in t1.
+		 */
+		if (curve.aIsM3) {
+			/*
+			 * Set t1 = Z^2.
+			 */
+			mt1.Set(mz);
+			mt1.MontySquare();
+
+			/*
+			 * Set t2 = X-Z^2 and then t1 = X+Z^2.
+			 */
+			mt2.Set(mx);
+			mt2.Sub(mt1);
+			mt1.Add(mx);
+
+			/*
+			 * Set t1 = 3*(X+Z^2)*(X-Z^2).
+			 */
+			mt1.MontyMul(mt2);
+			mt2.Set(mt1);
+			mt1.Add(mt2);
+			mt1.Add(mt2);
+		} else {
+			/*
+			 * Set t1 = 3*X^2.
+			 */
+			mt1.Set(mx);
+			mt1.MontySquare();
+			mt2.Set(mt1);
+			mt1.Add(mt2);
+			mt1.Add(mt2);
+
+			/*
+			 * Set t2 = a*Z^4.
+			 */
+			mt2.Set(mz);
+			mt2.MontySquare();
+			mt2.MontySquare();
+			mt2.MontyMul(curve.ma);
+
+			/*
+			 * Set t1 = 3*X^2 + a*Z^4.
+			 */
+			mt1.Add(mt2);
+		}
+
+		/*
+		 * Compute S = 4*X*Y^2 in t2. We also save 2*Y^2 in mt3.
+		 */
+		mt2.Set(my);
+		mt2.MontySquare();
+		mt2.Add(mt2);
+		mt3.Set(mt2);
+		mt2.Add(mt2);
+		mt2.MontyMul(mx);
+
+		/*
+		 * Compute X' = M^2 - 2*S.
+		 */
+		mx.Set(mt1);
+		mx.MontySquare();
+		mx.Sub(mt2);
+		mx.Sub(mt2);
+
+		/*
+		 * Compute Z' = 2*Y*Z.
+		 */
+		mz.MontyMul(my);
+		mz.Add(mz);
+
+		/*
+		 * Compute Y' = M*(S - X') - 8*Y^4. We already have
+		 * 4*Y^2 in t3.
+		 */
+		mt2.Sub(mx);
+		mt2.MontyMul(mt1);
+		mt3.MontySquare();
+		mt3.Add(mt3);
+		my.Set(mt2);
+		my.Sub(mt3);
+	}
+
+	internal override uint AddCT(MutableECPoint Q)
+	{
+		MutableECPointPrime P2 = SameCurve(Q);
+
+		if (P2.affine != 0) {
+			ms4.Set(P2.mx);
+			ms5.Set(P2.my);
+			ms6.Set(P2.mz);
+			ms4.ToMonty();
+			ms5.ToMonty();
+			ms6.SetMonty(~ms6.IsZeroCT);
+			return AddCTInner(ms4, ms5, ms6, true);
+		} else {
+			return AddCTInner(P2.mx, P2.my, P2.mz, false);
+		}
+	}
+
+	/*
+	 * Inner function for addition. The Jacobian coordinates for
+	 * the operand are provided in Montogomery representation. If
+	 * p2affine is true, then it is guaranteed that p2z is 1
+	 * (converted to Montogomery).
+	 */
+	uint AddCTInner(ModInt p2x, ModInt p2y, ModInt p2z, bool p2affine)
+	{
+		/*
+		 * In this comment, the two operands are called P1 and
+		 * P2. P1 is this instance; P2 is the operand. Coordinates
+		 * of P1 are (X1,Y1,Z1). Coordinates of P2 are (X2,Y2,Z2).
+		 *
+		 * Formulas:
+		 *   U1 = X1 * Z2^2
+		 *   U2 = X2 * Z1^2
+		 *   S1 = Y1 * Z2^3
+		 *   S2 = Y2 * Z1^3
+		 *   H = U2 - U1
+		 *   R = S2 - S1
+		 *   X3 = R^2 - H^3 - 2*U1*H^2
+		 *   Y3 = R*(U1*H^2 - X3) - S1*H^3
+		 *   Z3 = H*Z1*Z2
+		 *
+		 * If both P1 and P2 are 0, then the formulas yield 0,
+		 * which is fine. If one of P1 and P2 is 0 (but not both),
+		 * then we get 0 as result, which is wrong and must be
+		 * fixed at the end.
+		 *
+		 * If U1 == U2 and S1 == S2 then this means that either
+		 * P1 or P2 is 0 (or both), or P1 == P2. In the latter
+		 * case, the formulas are wrong and we must report
+		 * an error.
+		 *
+		 * If U1 == U2 and S1 != S2 then P1 + P2 = 0. We get H = 0,
+		 * which implies that we obtain the point at infinity,
+		 * which is fine.
+		 */
+
+		uint P1IsZero = mz.IsZeroCT;
+		uint P2IsZero = p2z.IsZeroCT;
+
+		ToJacobian();
+
+		/*
+		 * Save this value, in case the operand turns out to
+		 * be the point at infinity.
+		 */
+		ms1.Set(mx);
+		ms2.Set(my);
+		ms3.Set(mz);
+
+		/*
+		 * Compute U1 = X1*Z2^2 in t1, and S1 = Y1*Z2^3 in t3.
+		 */
+		if (p2affine) {
+			mt1.Set(mx);
+			mt3.Set(my);
+		} else {
+			mt3.Set(p2z);
+			mt3.MontySquare();
+			mt1.Set(mx);
+			mt1.MontyMul(mt3);
+			mt3.MontyMul(p2z);
+			mt3.MontyMul(my);
+		}
+		//PrintMR(" u1 = x1*z2^2", mt1);
+		//PrintMR(" s1 = y1*z2^3", mt3);
+
+		/*
+		 * Compute U2 = X2*Z1^2 in t2, and S2 = Y2*Z1^3 in t4.
+		 */
+		mt4.Set(mz);
+		mt4.MontySquare();
+		mt2.Set(p2x);
+		mt2.MontyMul(mt4);
+		mt4.MontyMul(mz);
+		mt4.MontyMul(p2y);
+		//PrintMR(" u2 = x2*z1^2", mt2);
+		//PrintMR(" s2 = y2*z1^3", mt4);
+
+		/*
+		 * Compute H = U2 - U1 in t2, and R = S2 - S1 in t4.
+		 */
+		mt2.Sub(mt1);
+		mt4.Sub(mt3);
+		//PrintMR(" h = u2-u1", mt2);
+		//PrintMR(" r = s2-s1", mt4);
+
+		/*
+		 * If both H and R are 0, then we may have a problem
+		 * (either P1 == P2, or P1 == 0, or P2 == 0).
+		 */
+		uint formProb = mt2.IsZeroCT & mt4.IsZeroCT;
+
+		/*
+		 * Compute U1*H^2 in t1 and H^3 in t5.
+		 */
+		mt5.Set(mt2);
+		mt5.MontySquare();
+		mt1.MontyMul(mt5);
+		mt5.MontyMul(mt2);
+		//PrintMR(" u1*h^2", mt1);
+		//PrintMR(" h^3", mt5);
+
+		/*
+		 * Compute X3 = R^2 - H^3 - 2*U1*H^2.
+		 */
+		mx.Set(mt4);
+		mx.MontySquare();
+		mx.Sub(mt5);
+		mx.Sub(mt1);
+		mx.Sub(mt1);
+		//PrintMR(" x3 = r^2-h^3-2*u1*h^2", mx);
+
+		/*
+		 * Compute Y3 = R*(U1*H^2 - X3) - S1*H^3.
+		 */
+		mt1.Sub(mx);
+		mt1.MontyMul(mt4);
+		mt5.MontyMul(mt3);
+		mt1.Sub(mt5);
+		my.Set(mt1);
+		//PrintMR(" y3 = r*(u1*h^2-x3)-s1*h^3", my);
+
+		/*
+		 * Compute Z3 = H*Z1*Z2.
+		 */
+		mz.MontyMul(mt2);
+		if (!p2affine) {
+			mz.MontyMul(p2z);
+		}
+		//PrintMR(" z3 = h*z1*z2", mz);
+
+		/*
+		 * Fixup: handle the cases where P1 = 0 or P2 = 0.
+		 */
+		mx.CondCopy(ms1, P2IsZero);
+		my.CondCopy(ms2, P2IsZero);
+		mz.CondCopy(ms3, P2IsZero);
+		mx.CondCopy(p2x, P1IsZero);
+		my.CondCopy(p2y, P1IsZero);
+		mz.CondCopy(p2z, P1IsZero);
+
+		/*
+		 * Report failure when P1 == P2, except when one of
+		 * the points was zero (or both) because that case
+		 * was properly handled.
+		 */
+		return (~formProb) | P1IsZero | P2IsZero;
+	}
+
+	internal override void NegCT()
+	{
+		my.Negate();
+	}
+
+	internal override uint MulSpecCT(byte[] n)
+	{
+		uint good = 0xFFFFFFFF;
+
+		/*
+		 * Create and populate window.
+		 *
+		 * If this instance is 0, then we only add 0 to 0 and
+		 * double 0, for which DoubleCT() and AddCT() work
+		 * properly.
+		 *
+		 * If this instance (P) is non-zero, then x*P for all
+		 * x in the 1..16 range shall be non-zero and distinct,
+		 * since the subgroup order is prime and at least 17.
+		 * Thus, we never add two equal points together in the
+		 * window construction.
+		 *
+		 * We MUST ensure that all points are in the same
+		 * coordinate convention (affine or Jacobian) to ensure
+		 * constant-time execution. TODO: measure to see which
+		 * is best: all affine or all Jacobian. All affine implies
+		 * 14 or 15 extra divisions, but saves a few hundreds of
+		 * multiplications.
+		 */
+		MutableECPointPrime[] w = new MutableECPointPrime[16];
+		w[0] = new MutableECPointPrime(curve);
+		w[0].ToJacobian();
+		w[1] = new MutableECPointPrime(curve);
+		w[1].Set(this);
+		w[1].ToJacobian();
+		for (int i = 2; (i + 1) < w.Length; i += 2) {
+			w[i] = new MutableECPointPrime(curve);
+			w[i].Set(w[i >> 1]);
+			w[i].DoubleCT();
+			w[i + 1] = new MutableECPointPrime(curve);
+			w[i + 1].Set(w[i]);
+			good &= w[i + 1].AddCT(this);
+		}
+
+		/* obsolete
+		for (int i = 0; i < w.Length; i ++) {
+			w[i].ToAffine();
+			w[i].Print("Win " + i);
+			w[i].ToJacobian();
+		}
+		Console.WriteLine("good = {0}", (int)good);
+		*/
+
+		/*
+		 * Set this value to 0. We also set it already to
+		 * Jacobian coordinates, since it will be done that
+		 * way anyway. This instance will serve as accumulator.
+		 */
+		mx.Set(0);
+		my.Set(0);
+		mz.Set(0);
+		affine = 0x00000000;
+
+		/*
+		 * We process the multiplier by 4-bit nibbles, starting
+		 * with the most-significant one (the high nibble of the
+		 * first byte, since we use big-endian notation).
+		 *
+		 * For each nibble, we perform a constant-time lookup
+		 * in the window, to obtain the point to add to the
+		 * current value of the accumulator. Thanks to the
+		 * conditions on the operands (prime subgroup order and
+		 * so on), all the additions below must work.
+		 */
+		MutableECPointPrime t = new MutableECPointPrime(curve);
+		for (int i = (n.Length << 1) - 1; i >= 0; i --) {
+			int b = n[n.Length - 1 - (i >> 1)];
+			int j = (b >> ((i & 1) << 2)) & 0x0F;
+			for (int k = 0; k < 16; k ++) {
+				t.Set(w[k], ~(uint)(((j - k) | (k - j)) >> 31));
+			}
+			good &= AddCT(t);
+			if (i > 0) {
+				DoubleCT();
+				DoubleCT();
+				DoubleCT();
+				DoubleCT();
+			}
+		}
+
+		return good;
+	}
+
+	internal override uint EqCT(MutableECPoint Q)
+	{
+		MutableECPointPrime R = SameCurve(Q);
+		if (affine != 0) {
+			if (R.affine != 0) {
+				return mx.EqCT(R.mx)
+					& my.EqCT(R.my)
+					& mz.EqCT(R.mz);
+			} else {
+				return EqCTMixed(R, this);
+			}
+		} else if (R.affine != 0) {
+			return EqCTMixed(this, R);
+		}
+
+		/*
+		 * Both points are in Jacobian coordinates.
+		 * If Z1 and Z2 are non-zero, then equality is
+		 * achieved if and only if both following equations
+		 * are true:
+		 *     X1*(Z2^2) = X2*(Z1^2)
+		 *     Y1*(Z2^3) = Y2*(Z1^3)
+		 * If Z1 or Z2 is zero, then equality is achieved
+		 * if and only if both are zero.
+		 */
+		mt1.Set(mz);
+		mt1.MontySquare();
+		mt2.Set(R.mz);
+		mt2.MontySquare();
+		mt3.Set(mx);
+		mt3.MontyMul(mt2);
+		mt4.Set(R.mx);
+		mt4.MontyMul(mt1);
+		uint r = mt3.EqCT(mt4);
+		mt1.MontyMul(mz);
+		mt2.MontyMul(R.mz);
+		mt3.Set(my);
+		mt3.MontyMul(mt2);
+		mt4.Set(R.my);
+		mt4.MontyMul(mt1);
+		r &= mt3.EqCT(mt4);
+		uint z1z = mz.IsZeroCT;
+		uint z2z = R.mz.IsZeroCT;
+		return (r & ~(z1z | z2z)) ^ (z1z & z2z);
+	}
+
+	/*
+	 * Mixed comparison: P1 is in Jacobian coordinates, P2 is in
+	 * affine coordinates.
+	 */
+	uint EqCTMixed(MutableECPointPrime P1, MutableECPointPrime P2)
+	{
+		/*
+		 * If either P1 or P2 is infinity, then they are equal
+		 * if and only if they both are infinity.
+		 *
+		 * If neither is infinity, then we must check the following:
+		 *    X1 = X2*(Z1^2)
+		 *    Y1 = Y2*(Z1^3)
+		 * Beware that X1, Y1 and Z1 are in Montgomery representation,
+		 * while X2 and Y2 are not.
+		 */
+		mt1.Set(P1.mz);
+		mt1.MontySquare();
+		mt2.Set(P2.mx);
+		mt2.MontyMul(mt1);
+		mt3.Set(P1.mx);
+		mt3.FromMonty();
+		uint r = mt2.EqCT(mt3);
+		mt1.MontyMul(P1.mz);
+		mt1.MontyMul(P2.my);
+		mt2.Set(P1.my);
+		mt2.FromMonty();
+		r &= mt1.EqCT(mt2);
+		uint z1z = P1.mz.IsZeroCT;
+		uint z2z = P2.mz.IsZeroCT;
+		return (r & ~(z1z | z2z)) ^ (z1z & z2z);
+	}
+
+	MutableECPointPrime SameCurve(MutableECPoint Q)
+	{
+		MutableECPointPrime R = Q as MutableECPointPrime;
+		if (R == null || !curve.Equals(R.curve)) {
+			throw new CryptoException("Mixed curves");
+		}
+		return R;
+	}
+
+	/*
+	 * Convert to Jabobian coordinates (if not already done).
+	 */
+	void ToJacobian()
+	{
+		if (affine == 0) {
+			return;
+		}
+
+		/*
+		 * Since Z = 0 or 1 in affine coordinates, we can
+		 * use SetMonty().
+		 */
+		mx.ToMonty();
+		my.ToMonty();
+		mz.SetMonty(~mz.IsZeroCT);
+		affine = 0x00000000;
+	}
+
+	/*
+	 * Convert to affine coordinates (if not already done).
+	 */
+	void ToAffine()
+	{
+		if (affine != 0) {
+			return;
+		}
+
+		/*
+		 * Divisions are expensive, so we want to make only one,
+		 * not two. This involves some games with Montgomery
+		 * representation.
+		 *
+		 * A number a in Montgomery representation means that
+		 * the value we have is equal to aR. Montgomery
+		 * multiplication of a by b yields ab/R (so, if we
+		 * apply it to aR and bR, we get abR).
+		 */
+
+		/* Save Z*R in mt1. */
+		mt1.Set(mz);
+
+		/* Compute Z^3 in mz. */
+		mz.MontySquare();
+		mz.MontyMul(mt1);
+		mz.FromMonty();
+
+		/* Compute t2 = 1/Z^3. */
+		mt2.Set(mz);
+		mt2.Invert();
+		uint cc = ~mt2.IsZeroCT;
+		
+		/* Compute y. */
+		my.MontyMul(mt2);
+
+		/* Compute t2 = 1/Z^2. */
+		mt2.MontyMul(mt1);
+
+		/* Compute x. */
+		mx.MontyMul(mt2);
+
+		/*
+		 * If the point is infinity (division by Z^2 failed),
+		 * then set all coordinates to 0. Otherwise, set mz
+		 * to exactly 1.
+		 */
+		mx.CondCopy(curve.mp, ~cc);
+		my.CondCopy(curve.mp, ~cc);
+		mz.Set((int)cc & 1);
+
+		affine = 0xFFFFFFFF;
+	}
+
+	/*
+	 * Compute Y^2 into my, using the value in mx as X. Both values
+	 * are in normal (non-Montgomery) representation.
+	 */
+	void RebuildY2()
+	{
+		my.Set(mx);
+		my.ToMonty();
+		mt1.Set(my);
+		my.MontySquare();
+		my.MontyMul(mx);
+		mt1.MontyMul(curve.ma);
+		my.Add(mt1);
+		my.Add(curve.mb);
+	}
+}
+
+}
diff --git a/Crypto/NIST.cs b/Crypto/NIST.cs
new file mode 100644
index 0000000..b21f277
--- /dev/null
+++ b/Crypto/NIST.cs
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * This class contains static definitions for the NIST elliptic
+ * curves.
+ */
+
+public class NIST {
+
+	// public static ECCurve P192;
+	// public static ECCurve P224;
+	public static ECCurve P256;
+	public static ECCurve P384;
+	public static ECCurve P521;
+
+	static NIST()
+	{
+		P256 = MakePrime(
+			"P-256",
+			"FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF",
+			"FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC",
+			"5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B",
+			"6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296",
+			"4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5",
+			"FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551",
+			"01");
+		P384 = MakePrime(
+			"P-384",
+			"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF",
+			"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC",
+			"B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF",
+			"AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7",
+			"3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F",
+			"FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973",
+			"01");
+		P521 = MakePrime(
+			"P-521",
+			"01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF",
+			"01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC",
+			"0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00",
+			"00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66",
+			"011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650",
+			"01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409",
+			"01");
+	}
+
+	static ECCurvePrime MakePrime(string name, string smod,
+		string sa, string sb, string sgx, string sgy,
+		string sso, string scf)
+	{
+		return new ECCurvePrime(
+			name,
+			ToBytes(smod), ToBytes(sa), ToBytes(sb),
+			ToBytes(sgx), ToBytes(sgy),
+			ToBytes(sso), ToBytes(scf));
+	}
+
+	static byte[] ToBytes(string s)
+	{
+		int len = ToBytes(s, null);
+		byte[] dst = new byte[len];
+		ToBytes(s, dst);
+		return dst;
+	}
+
+	static int ToBytes(string str, byte[] dst)
+	{
+		int off = 0;
+		bool z = true;
+		int acc = 0;
+		foreach (char c in str) {
+			int d;
+			if (c >= '0' && c <= '9') {
+				d = c - '0';
+			} else if (c >= 'A' && c <= 'F') {
+				d = c - ('A' - 10);
+			} else if (c >= 'a' && c <= 'f') {
+				d = c - ('a' - 10);
+			} else if (c == ' ' || c == '\t' || c == ':') {
+				continue;
+			} else {
+				throw new ArgumentException(String.Format(
+					"not hex: U+{0:X4}", (int)c));
+			}
+			if (z) {
+				acc = d;
+			} else {
+				if (dst != null) {
+					dst[off] = (byte)((acc << 4) + d);
+				}
+				off ++;
+			}
+			z = !z;
+		}
+		if (!z) {
+			throw new ArgumentException("final half byte");
+		}
+		return off;
+	}
+}
+
+}
diff --git a/Crypto/Poly1305.cs b/Crypto/Poly1305.cs
new file mode 100644
index 0000000..fb63429
--- /dev/null
+++ b/Crypto/Poly1305.cs
@@ -0,0 +1,327 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * This class implements Poly1305, when used with ChaCha20. The
+ * ChaCha20 instance (already set with the secret key) is passed as
+ * parameter. Instances are not thread-safe.
+ */
+
+public sealed class Poly1305 {
+
+	/*
+	 * The ChaCha20 instance to use for encryption and decryption.
+	 * That instance MUST be set, and it must have been initialised
+	 * with the key to use.
+	 */
+	public ChaCha20 ChaCha {
+		get; set;
+	}
+
+	byte[] pkey;
+	uint[] r;
+	uint[] acc;
+	byte[] foot;
+	byte[] tmp;
+
+	/*
+	 * Create a new instance. The ChaCha20 instance to use MUST be
+	 * set in the 'ChaCha' property.
+	 */
+	public Poly1305()
+	{
+		pkey = new byte[32];
+		r = new uint[5];
+		acc = new uint[5];
+		foot = new byte[16];
+		tmp = new byte[16];
+	}
+
+	/*
+	 * Run Poly1305 and ChaCha20 on the provided elements.
+	 *
+	 *   iv        Nonce for ChaCha20, exactly 12 bytes
+	 *   data      data to encrypt or decrypt (buffer, off + len)
+	 *   aad       additional authenticated data (buffer, offAAD + lenAAD)
+	 *   tag       destination for computed authentication tag
+	 *   encrypt   true to encrypt, false to decrypt
+	 */
+	public void Run(byte[] iv,
+		byte[] data, int off, int len,
+		byte[] aad, int offAAD, int lenAAD,
+		byte[] tag, bool encrypt)
+	{
+		/*
+		 * Compute Poly1305 key.
+		 */
+		for (int i = 0; i < pkey.Length; i ++) {
+			pkey[i] = 0;
+		}
+		ChaCha.Run(iv, 0, pkey);
+
+		/*
+		 * If encrypting, ChaCha20 must run first (the MAC is
+		 * computed on the ciphertext).
+		 */
+		if (encrypt) {
+			ChaCha.Run(iv, 1, data, off, len);
+		}
+
+		/*
+		 * Decode the 'r' value into 26-bit words, with the
+		 * "clamping" operation applied.
+		 */
+		r[0] = Dec32le(pkey, 0) & 0x03FFFFFF;
+		r[1] = (Dec32le(pkey, 3) >> 2) & 0x03FFFF03;
+		r[2] = (Dec32le(pkey,  6) >> 4) & 0x03FFC0FF;
+		r[3] = (Dec32le(pkey,  9) >> 6) & 0x03F03FFF;
+		r[4] = (Dec32le(pkey, 12) >> 8) & 0x000FFFFF;
+
+		/*
+		 * Accumulator is 0.
+		 */
+		acc[0] = 0;
+		acc[1] = 0;
+		acc[2] = 0;
+		acc[3] = 0;
+		acc[4] = 0;
+
+		/*
+		 * Process AAD, ciphertext and footer.
+		 */
+		Enc32le(lenAAD, foot, 0);
+		Enc32le(0, foot, 4);
+		Enc32le(len, foot, 8);
+		Enc32le(0, foot, 12);
+		RunInner(aad, offAAD, lenAAD);
+		RunInner(data, off, len);
+		RunInner(foot, 0, 16);
+
+		/*
+		 * Finalize modular reduction. The output of RunInner() is
+		 * already mostly reduced: only acc[1] may be (very slightly)
+		 * above 2^26. Thus, we only need one loop, back to acc[1].
+		 */
+		uint cc = 0;
+		for (int i = 1; i <= 6; i ++) {
+			int j;
+
+			j = (i >= 5) ? i - 5 : i;
+			acc[j] += cc;
+			cc = acc[j] >> 26;
+			acc[j] &= 0x03FFFFFF;
+		}
+
+		/*
+		 * The final value may still be in the 2^130-5..2^130-1
+		 * range, in which case an additional subtraction must be
+		 * performed, with constant-time code.
+		 */
+		cc = (uint)((int)(0x03FFFFFA - acc[0]) >> 31);
+		for (int i = 1; i < 5; i ++) {
+			int z = (int)(acc[i] - 0x03FFFFFF);
+			cc &= ~(uint)((z | -z) >> 31);
+		}
+		cc &= 5;
+		for (int i = 0; i < 5; i ++) {
+			uint t = acc[i] + cc;
+			cc = t >> 26;
+			acc[i] = t & 0x03FFFFFF;
+		}
+
+		/*
+		 * The tag is the sum of the 's' value (second half of
+		 * the pkey[] array, little-endian encoding) and the
+		 * current accumulator value. This addition is done modulo
+		 * 2^128, i.e. with a simple truncation.
+		 */
+		cc = 0;
+		uint aw = 0;
+		int awLen = 0;
+		for (int i = 0, j = 0; i < 16; i ++) {
+			if (awLen < 8) {
+				/*
+				 * We "refill" our running byte buffer with
+				 * a new extra accumulator word. Note that
+				 * 'awLen' is always even, so at this point
+				 * it must be 6 or less; since accumulator
+				 * words fit on 32 bits, the operation
+				 * below does not lose any bit.
+				 */
+				aw |= acc[j ++] << awLen;
+				awLen += 26;
+			}
+			uint tb = (aw & 0xFF) + cc + pkey[16 + i];
+			aw >>= 8;
+			awLen -= 8;
+			tag[i] = (byte)tb;
+			cc = tb >> 8;
+		}
+
+		/*
+		 * If decrypting, then we still have the ciphertext at
+		 * this point, and we must perform the decryption.
+		 */
+		if (!encrypt) {
+			ChaCha.Run(iv, 1, data, off, len);
+		}
+	}
+
+	/*
+	 * Inner processing of the provided data. The accumulator and 'r'
+	 * value are set in the instance fields, and must be updated.
+	 * All accumulator words fit on 26 bits each, except the second
+	 * (acc[1]) which may be very slightly above 2^26.
+	 */
+	void RunInner(byte[] data, int off, int len)
+	{
+		/*
+		 * Implementation is inspired from the public-domain code
+		 * available there:
+		 *    https://github.com/floodyberry/poly1305-donna
+		 */
+		uint r0 = r[0];
+		uint r1 = r[1];
+		uint r2 = r[2];
+		uint r3 = r[3];
+		uint r4 = r[4];
+
+		uint u1 = r1 * 5;
+		uint u2 = r2 * 5;
+		uint u3 = r3 * 5;
+		uint u4 = r4 * 5;
+
+		uint a0 = acc[0];
+		uint a1 = acc[1];
+		uint a2 = acc[2];
+		uint a3 = acc[3];
+		uint a4 = acc[4];
+
+		while (len > 0) {
+			if (len < 16) {
+				Array.Copy(data, off, tmp, 0, len);
+				for (int i = len; i < 16; i ++) {
+					tmp[i] = 0;
+				}
+				data = tmp;
+				off = 0;
+			}
+
+			/*
+			 * Decode next block, with the "high bit" applied,
+			 * and add that value to the accumulator.
+			 */
+			a0 += Dec32le(data, off) & 0x03FFFFFF;
+			a1 += (Dec32le(data, off +  3) >> 2) & 0x03FFFFFF;
+			a2 += (Dec32le(data, off +  6) >> 4) & 0x03FFFFFF;
+			a3 += (Dec32le(data, off +  9) >> 6) & 0x03FFFFFF;
+			a4 += (Dec32le(data, off + 12) >> 8) | 0x01000000;
+
+			/*
+			 * Compute multiplication. All elementary
+			 * multiplications are 32x32->64.
+			 */
+			ulong w0 = (ulong)a0 * r0
+				+ (ulong)a1 * u4
+				+ (ulong)a2 * u3
+				+ (ulong)a3 * u2
+				+ (ulong)a4 * u1;
+			ulong w1 = (ulong)a0 * r1
+				+ (ulong)a1 * r0
+				+ (ulong)a2 * u4
+				+ (ulong)a3 * u3
+				+ (ulong)a4 * u2;
+			ulong w2 = (ulong)a0 * r2
+				+ (ulong)a1 * r1
+				+ (ulong)a2 * r0
+				+ (ulong)a3 * u4
+				+ (ulong)a4 * u3;
+			ulong w3 = (ulong)a0 * r3
+				+ (ulong)a1 * r2
+				+ (ulong)a2 * r1
+				+ (ulong)a3 * r0
+				+ (ulong)a4 * u4;
+			ulong w4 = (ulong)a0 * r4
+				+ (ulong)a1 * r3
+				+ (ulong)a2 * r2
+				+ (ulong)a3 * r1
+				+ (ulong)a4 * r0;
+
+			/*
+			 * Most of the modular reduction was done by using
+			 * the 'u*' multipliers. We still need to do some
+			 * carry propagation.
+			 */
+			ulong c;
+			c = w0 >> 26;
+			a0 = (uint)w0 & 0x03FFFFFF;
+			w1 += c;
+			c = w1 >> 26;
+			a1 = (uint)w1 & 0x03FFFFFF;
+			w2 += c;
+			c = w2 >> 26;
+			a2 = (uint)w2 & 0x03FFFFFF;
+			w3 += c;
+			c = w3 >> 26;
+			a3 = (uint)w3 & 0x03FFFFFF;
+			w4 += c;
+			c = w4 >> 26;
+			a4 = (uint)w4 & 0x03FFFFFF;
+			a0 += (uint)c * 5;
+			a1 += a0 >> 26;
+			a0 &= 0x03FFFFFF;
+
+			off += 16;
+			len -= 16;
+		}
+
+		acc[0] = a0;
+		acc[1] = a1;
+		acc[2] = a2;
+		acc[3] = a3;
+		acc[4] = a4;
+	}
+
+	static uint Dec32le(byte[] buf, int off)
+	{
+		return (uint)buf[off]
+			| ((uint)buf[off + 1] << 8)
+			| ((uint)buf[off + 2] << 16)
+			| ((uint)buf[off + 3] << 24);
+	}
+
+	static void Enc32le(int x, byte[] buf, int off)
+	{
+		buf[off] = (byte)x;
+		buf[off + 1] = (byte)(x >> 8);
+		buf[off + 2] = (byte)(x >> 16);
+		buf[off + 3] = (byte)(x >> 24);
+	}
+}
+
+}
diff --git a/Crypto/RFC6979.cs b/Crypto/RFC6979.cs
new file mode 100644
index 0000000..03a77b9
--- /dev/null
+++ b/Crypto/RFC6979.cs
@@ -0,0 +1,103 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * This class implements the computation of the transient secret value
+ * "k" for DSA and ECDSA, using the method described in RFC 6979. It
+ * can perform the deterministic computation, and optionally inject
+ * extra random bytes when randomized signatures are needed.
+ */
+
+class RFC6979 {
+
+	HMAC_DRBG drbg;
+	byte[] q;
+	int qlen;
+	ModInt mh;
+
+	internal RFC6979(IDigest h, byte[] q, byte[] x,
+		byte[] hv, bool deterministic)
+		: this(h, q, x, hv, 0, hv.Length, deterministic)
+	{
+	}
+
+	internal RFC6979(IDigest h, byte[] q, byte[] x,
+		byte[] hv, int hvOff, int hvLen, bool deterministic)
+	{
+		if (h == null) {
+			h = new SHA256();
+		} else {
+			h = h.Dup();
+			h.Reset();
+		}
+		drbg = new HMAC_DRBG(h);
+		mh = new ModInt(q);
+		qlen = mh.ModBitLength;
+		int qolen = (qlen + 7) >> 3;
+		this.q = new byte[qolen];
+		Array.Copy(q, q.Length - qolen, this.q, 0, qolen);
+		int hlen = hvLen << 3;
+		if (hlen > qlen) {
+			byte[] htmp = new byte[hvLen];
+			Array.Copy(hv, hvOff, htmp, 0, hv.Length);
+			BigInt.RShift(htmp, hlen - qlen);
+			hv = htmp;
+			hvOff = 0;
+		}
+		mh.DecodeReduce(hv, hvOff, hvLen);
+		ModInt mx = mh.Dup();
+		mx.Decode(x);
+
+		byte[] seed = new byte[(qolen << 1) + (deterministic ? 0 : 32)];
+		mx.Encode(seed, 0, qolen);
+		mh.Encode(seed, qolen, qolen);
+		if (!deterministic) {
+			RNG.GetBytes(seed, qolen << 1,
+				seed.Length - (qolen << 1));
+		}
+		drbg.SetSeed(seed);
+	}
+
+	internal ModInt GetHashMod()
+	{
+		return mh.Dup();
+	}
+
+	internal void NextK(byte[] k)
+	{
+		for (;;) {
+			drbg.GetBytes(k);
+			BigInt.RShift(k, (k.Length << 3) - qlen);
+			if (!BigInt.IsZero(k) && BigInt.CompareCT(k, q) < 0) {
+				return;
+			}
+		}
+	}
+}
+
+}
diff --git a/Crypto/RNG.cs b/Crypto/RNG.cs
new file mode 100644
index 0000000..ba4d5a9
--- /dev/null
+++ b/Crypto/RNG.cs
@@ -0,0 +1,224 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Text;
+
+using NC = System.Security.Cryptography;
+
+namespace Crypto {
+
+/*
+ * Random generator.
+ */
+
+public sealed class RNG {
+
+	/*
+	 * To ensure efficient generation of random numbers, we use
+	 * our own PRNG, seeded with a strong value (from the operating
+	 * system), and based on AES-CTR. We obtain a random 128-bit
+	 * key from the OS (RNGCryptoServiceProvider); then we use it
+	 * to encrypt successive values for a 128-bit counter (also
+	 * initialized from RNGCryptoServiceProvider). This is AES-CTR
+	 * mode and thus provably as strong as AES-128 encryption as it
+	 * is practiced in SSL/TLS.
+	 *
+	 * A mutex is used to ensure safe access in a multi-threaded
+	 * context. Once initialized, random generation proceeds at
+	 * the same speed as AES encryption, i.e. fast enough for
+	 * our purposes.
+	 *
+	 * As a special action for debugging, it is possible to reset
+	 * the state to an explicit seed value. Of course, this tends
+	 * to kill security, so it should be used only to make actions
+	 * reproducible, as part of systematic tests.
+	 */
+
+	static object rngMutex = new object();
+	static IBlockCipher rngAES = null;
+	static byte[] counter, rblock;
+
+	static void Init()
+	{
+		if (rngAES == null) {
+			NC.RNGCryptoServiceProvider srng =
+				new NC.RNGCryptoServiceProvider();
+			byte[] key = new byte[16];
+			byte[] iv = new byte[16];
+			srng.GetBytes(key);
+			srng.GetBytes(iv);
+			Init(key, iv);
+		}
+	}
+
+	static void Init(byte[] key, byte[] iv)
+	{
+		if (rngAES == null) {
+			rngAES = new AES();
+			counter = new byte[16];
+			rblock = new byte[16];
+		}
+		rngAES.SetKey(key);
+		Array.Copy(iv, 0, rblock, 0, 16);
+	}
+
+	static void NextBlock()
+	{
+		int len = counter.Length;
+		int carry = 1;
+		for (int i = 0; i < len; i ++) {
+			int v = counter[i] + carry;
+			counter[i] = (byte)v;
+			carry = v >> 8;
+		}
+		Array.Copy(counter, 0, rblock, 0, len);
+		rngAES.BlockEncrypt(rblock);
+	}
+
+	/*
+	 * Set or reset the state to the provided seed. All subsequent
+	 * output will depend only on that seed value. This function shall
+	 * be used ONLY for debug/test purposes, since it replaces the
+	 * automatic seeding that uses OS-provided entropy.
+	 */
+	public static void SetSeed(byte[] seed)
+	{
+		byte[] s32 = new SHA256().Hash(seed);
+		byte[] key = new byte[16];
+		byte[] iv = new byte[16];
+		Array.Copy(s32, 0, key, 0, 16);
+		Array.Copy(s32, 16, iv, 0, 16);
+		lock (rngMutex) {
+			Init(key, iv);
+		}
+	}
+
+	/*
+	 * Fill the provided array with random bytes.
+	 */
+	public static void GetBytes(byte[] buf)
+	{
+		GetBytes(buf, 0, buf.Length);
+	}
+
+	/*
+	 * Fill the provided array chunk with random bytes.
+	 */
+	public static void GetBytes(byte[] buf, int off, int len)
+	{
+		lock (rngMutex) {
+			Init();
+			while (len > 0) {
+				NextBlock();
+				int clen = Math.Min(len, rblock.Length);
+				Array.Copy(rblock, 0, buf, off, clen);
+				off += clen;
+				len -= clen;
+			}
+		}
+	}
+
+	/*
+	 * Get a new random 32-bit integer (uniform generation).
+	 */
+	public static uint U32()
+	{
+		lock (rngMutex) {
+			Init();
+			NextBlock();
+			return (uint)rblock[0]
+				| ((uint)rblock[1] << 8)
+				| ((uint)rblock[2] << 16)
+				| ((uint)rblock[3] << 24);
+		}
+	}
+
+	/*
+	 * Convert integer value x (0 to 15) to an hexadecimal character
+	 * (lowercase).
+	 */
+	static char ToHex(int x)
+	{
+		int hi = -(((x + 6) >> 4) & 1);
+		return (char)(x + 48 + (hi & 39));
+	}
+
+	/*
+	 * Get a string of random hexadecimal characters. The 'len'
+	 * parameter specifies the string length in characters (it
+	 * may be odd).
+	 */
+	public static string GetHex(int len)
+	{
+		byte[] buf = new byte[(len + 1) >> 1];
+		GetBytes(buf);
+		StringBuilder sb = new StringBuilder();
+		foreach (byte b in buf) {
+			sb.Append(b >> 4);
+			sb.Append(b & 15);
+		}
+		string s = sb.ToString();
+		if (s.Length > len) {
+			s = s.Substring(0, len);
+		}
+		return s;
+	}
+
+	/*
+	 * Get a sequence of random non-zero bytes.
+	 */
+	public static void GetBytesNonZero(byte[] buf)
+	{
+		GetBytesNonZero(buf, 0, buf.Length);
+	}
+
+	/*
+	 * Get a sequence of random non-zero bytes.
+	 */
+	public static void GetBytesNonZero(byte[] buf, int off, int len)
+	{
+		if (len <= 0) {
+			return;
+		}
+		lock (rngMutex) {
+			Init();
+			for (;;) {
+				NextBlock();
+				for (int i = 0; i < rblock.Length; i ++) {
+					byte x = rblock[i];
+					if (x == 0) {
+						continue;
+					}
+					buf[off ++] = x;
+					if (-- len == 0) {
+						return;
+					}
+				}
+			}
+		}
+	}
+}
+
+}
diff --git a/Crypto/RSA.cs b/Crypto/RSA.cs
new file mode 100644
index 0000000..72d95ed
--- /dev/null
+++ b/Crypto/RSA.cs
@@ -0,0 +1,560 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * This class implements the RSA encryption and signature algorithms.
+ * The static methods provide access to the algorithm primitives. For
+ * signatures, the hash of the signed data must be provided externally.
+ */
+
+public static class RSA {
+
+	/*
+	 * Get the maximum length (in bytes) of an RSA-encrypted value
+	 * with the specified public key.
+	 */
+	public static int GetMaxEncryptedLength(RSAPublicKey pk)
+	{
+		return pk.Modulus.Length;
+	}
+
+	/*
+	 * Encrypt a message with a public key. This applied PKCS#1 v1.5
+	 * "type 2" padding. If the message length exceeds the maximum
+	 * that can be processed with that public key, an exception is
+	 * thrown.
+	 *
+	 * There are four methods, depending on the kind of source
+	 * operand, and how the destination is to be obtained.
+	 */
+
+	public static byte[] Encrypt(RSAPublicKey pk, byte[] buf)
+	{
+		return Encrypt(pk, buf, 0, buf.Length);
+	}
+
+	public static byte[] Encrypt(RSAPublicKey pk,
+		byte[] buf, int off, int len)
+	{
+		byte[] n = pk.Modulus;
+		int modLen = n.Length;
+		byte[] x = DoPKCS1Padding(modLen, true, null, buf, off, len);
+		return BigInt.ModPow(x, pk.Exponent, n);
+	}
+
+	public static int Encrypt(RSAPublicKey pk,
+		byte[] buf, byte[] outBuf, int outOff)
+	{
+		return Encrypt(pk, buf, 0, buf.Length, outBuf, outOff);
+	}
+
+	public static int Encrypt(RSAPublicKey pk,
+		byte[] buf, int off, int len, byte[] outBuf, int outOff)
+	{
+		byte[] r = Encrypt(pk, buf, off, len);
+		Array.Copy(r, 0, outBuf, outOff, r.Length);
+		return r.Length;
+	}
+
+	/*
+	 * Perform a RSA decryption. A PKCS#1 v1.5 "type 2" padding is
+	 * expected, and removed. An exception is thrown on any error.
+	 *
+	 * WARNING: potentially vulnerable to Bleichenbacher's attack.
+	 * Use with care.
+	 *
+	 * There are four methods, depending on input and output
+	 * operands.
+	 */
+	public static byte[] Decrypt(RSAPrivateKey sk, byte[] buf)
+	{
+		return Decrypt(sk, buf, 0, buf.Length);
+	}
+
+	public static byte[] Decrypt(RSAPrivateKey sk,
+		byte[] buf, int off, int len)
+	{
+		byte[] tmp = new byte[sk.N.Length];
+		int outLen = Decrypt(sk, buf, off, len, tmp, 0);
+		byte[] outBuf = new byte[outLen];
+		Array.Copy(tmp, 0, outBuf, 0, outLen);
+		return outBuf;
+	}
+
+	public static int Decrypt(RSAPrivateKey sk,
+		byte[] buf, byte[] outBuf, int outOff)
+	{
+		return Decrypt(sk, buf, 0, buf.Length, outBuf, outOff);
+	}
+
+	public static int Decrypt(RSAPrivateKey sk,
+		byte[] buf, int off, int len, byte[] outBuf, int outOff)
+	{
+		if (len != sk.N.Length) {
+			throw new CryptoException(
+				"Invalid RSA-encrypted message length");
+		}
+
+		/*
+		 * Note: since RSAPrivateKey refuses a modulus of less
+		 * than 64 bytes, we know that len >= 64 here.
+		 */
+		byte[] x = new byte[len];
+		Array.Copy(buf, off, x, 0, len);
+		DoPrivate(sk, x);
+		if (x[0] != 0x00 || x[1] != 0x02) {
+			throw new CryptoException(
+				"Invalid PKCS#1 v1.5 encryption padding");
+		}
+		int i;
+		for (i = 2; i < len && x[i] != 0x00; i ++);
+		if (i < 10 || i >= len) {
+			throw new CryptoException(
+				"Invalid PKCS#1 v1.5 encryption padding");
+		}
+		i ++;
+		int olen = len - i;
+		Array.Copy(x, i, outBuf, outOff, olen);
+		return olen;
+	}
+
+	/*
+	 * Perform a RSA private key operation (modular exponentiation
+	 * with the private exponent). The source array MUST have the
+	 * same length as the modulus, and it is modified "in place".
+	 *
+	 * This function is constant-time, except if the source x[] does
+	 * not have the proper length (it should be identical to the
+	 * modulus length). If the source array has the proper length
+	 * but the numerical value is not in the proper range, then it
+	 * is first reduced modulo N.
+	 */
+	public static void DoPrivate(RSAPrivateKey sk, byte[] x)
+	{
+		DoPrivate(sk, x, 0, x.Length);
+	}
+
+	public static void DoPrivate(RSAPrivateKey sk,
+		byte[] x, int off, int len)
+	{
+		/*
+		 * Check that the source array has the proper length
+		 * (identical to the length of the modulus).
+		 */
+		if (len != sk.N.Length) {
+			throw new CryptoException(
+				"Invalid source length for RSA private");
+		}
+
+		/*
+		 * Reduce the source value to the proper range.
+		 */
+		ModInt mx = new ModInt(sk.N);
+		mx.DecodeReduce(x, off, len);
+
+		/*
+		 * Compute m1 = x^dp mod p.
+		 */
+		ModInt m1 = new ModInt(sk.P);
+		m1.Set(mx);
+		m1.Pow(sk.DP);
+
+		/*
+		 * Compute m2 = x^dq mod q.
+		 */
+		ModInt m2 = new ModInt(sk.Q);
+		m2.Set(mx);
+		m2.Pow(sk.DQ);
+
+		/*
+		 * Compute h = (m1 - m2) / q mod p.
+		 * (Result goes in m1.)
+		 */
+		ModInt m3 = m1.Dup();
+		m3.Set(m2);
+		m1.Sub(m3);
+		m3.Decode(sk.IQ);
+		m1.ToMonty();
+		m1.MontyMul(m3);
+
+		/*
+		 * Compute m_2 + q*h. This works on plain integers, but
+		 * we have efficient and constant-time code for modular
+		 * integers, so we will do it modulo n.
+		 */
+		m3 = mx;
+		m3.Set(m1);
+		m1 = m3.Dup();
+		m1.Decode(sk.Q);
+		m1.ToMonty();
+		m3.MontyMul(m1);
+		m1.Set(m2);
+		m3.Add(m1);
+
+		/*
+		 * Write result back in x[].
+		 */
+		m3.Encode(x, off, len);
+	}
+
+	/*
+	 * Constant headers for PKCS#1 v1.5 "type 1" padding. There are
+	 * two versions for each header, because of the PKCS#1 ambiguity
+	 * with regards to hash function parameters (ASN.1 NULL value,
+	 * or omitted).
+	 */
+
+	// PKCS#1 with no explicit digest function (special for SSL/TLS)
+	public static byte[] PKCS1_ND = new byte[] { };
+
+	// PKCS#1 with MD5
+	public static byte[] PKCS1_MD5 = new byte[] {
+		0x30, 0x20, 0x30, 0x0C, 0x06, 0x08, 0x2A, 0x86,
+		0x48, 0x86, 0xF7, 0x0D, 0x02, 0x05, 0x05, 0x00,
+		0x04, 0x10
+	};
+
+	// PKCS#1 with MD5 (alt)
+	public static byte[] PKCS1_MD5_ALT = new byte[] {
+		0x30, 0x1E, 0x30, 0x0A, 0x06, 0x08, 0x2A, 0x86,
+		0x48, 0x86, 0xF7, 0x0D, 0x02, 0x05, 0x04, 0x10
+	};
+
+	// PKCS#1 with SHA-1
+	public static byte[] PKCS1_SHA1 = new byte[] {
+		0x30, 0x21, 0x30, 0x09, 0x06, 0x05, 0x2B, 0x0E,
+		0x03, 0x02, 0x1A, 0x05, 0x00, 0x04, 0x14
+	};
+
+	// PKCS#1 with SHA-1 (alt)
+	public static byte[] PKCS1_SHA1_ALT = new byte[] {
+		0x30, 0x1F, 0x30, 0x07, 0x06, 0x05, 0x2B, 0x0E,
+		0x03, 0x02, 0x1A, 0x04, 0x14
+	};
+
+	// PKCS#1 with SHA-224
+	public static byte[] PKCS1_SHA224 = new byte[] {
+		0x30, 0x2D, 0x30, 0x0D, 0x06, 0x09, 0x60, 0x86,
+		0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04, 0x05,
+		0x00, 0x04, 0x1C
+	};
+
+	// PKCS#1 with SHA-224 (alt)
+	public static byte[] PKCS1_SHA224_ALT = new byte[] {
+		0x30, 0x2B, 0x30, 0x0B, 0x06, 0x09, 0x60, 0x86,
+		0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04, 0x04,
+		0x1C
+	};
+
+	// PKCS#1 with SHA-256
+	public static byte[] PKCS1_SHA256 = new byte[] {
+		0x30, 0x31, 0x30, 0x0D, 0x06, 0x09, 0x60, 0x86,
+		0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05,
+		0x00, 0x04, 0x20
+	};
+
+	// PKCS#1 with SHA-256 (alt)
+	public static byte[] PKCS1_SHA256_ALT = new byte[] {
+		0x30, 0x2F, 0x30, 0x0B, 0x06, 0x09, 0x60, 0x86,
+		0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x04,
+		0x20
+	};
+
+	// PKCS#1 with SHA-384
+	public static byte[] PKCS1_SHA384 = new byte[] {
+		0x30, 0x41, 0x30, 0x0D, 0x06, 0x09, 0x60, 0x86,
+		0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x05,
+		0x00, 0x04, 0x30
+	};
+
+	// PKCS#1 with SHA-384 (alt)
+	public static byte[] PKCS1_SHA384_ALT = new byte[] {
+		0x30, 0x3F, 0x30, 0x0B, 0x06, 0x09, 0x60, 0x86,
+		0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02, 0x04,
+		0x30
+	};
+
+	// PKCS#1 with SHA-512
+	public static byte[] PKCS1_SHA512 = new byte[] {
+		0x30, 0x51, 0x30, 0x0D, 0x06, 0x09, 0x60, 0x86,
+		0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x05,
+		0x00, 0x04, 0x40
+	};
+
+	// PKCS#1 with SHA-512 (alt)
+	public static byte[] PKCS1_SHA512_ALT = new byte[] {
+		0x30, 0x4F, 0x30, 0x0B, 0x06, 0x09, 0x60, 0x86,
+		0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03, 0x04,
+		0x40
+	};
+
+	/*
+	 * Verify an "ND" signature (no digest header: this is for
+	 * signatures in SSL/TLS up to TLS-1.1). The hash value
+	 * (normally the 36-byte concatenation of MD5 and SHA-1 in
+	 * the case of SSL/TLS) and the signature are provided.
+	 * Returned value is true on success, false otherwise. No
+	 * exception is thrown even if the public key is invalid.
+	 */
+
+	public static bool VerifyND(RSAPublicKey pk,
+		byte[] hash, byte[] sig)
+	{
+		return Verify(pk, PKCS1_ND, null,
+			hash, 0, hash.Length,
+			sig, 0, sig.Length);
+	}
+
+	public static bool VerifyND(RSAPublicKey pk,
+		byte[] hash, int hashOff, int hashLen, byte[] sig)
+	{
+		return Verify(pk, PKCS1_ND, null,
+			hash, hashOff, hashLen,
+			sig, 0, sig.Length);
+	}
+
+	public static bool VerifyND(RSAPublicKey pk,
+		byte[] hash, byte[] sig, int sigOff, int sigLen)
+	{
+		return Verify(pk, PKCS1_ND, null,
+			hash, 0, hash.Length,
+			sig, sigOff, sigLen);
+	}
+
+	public static bool VerifyND(RSAPublicKey pk,
+		byte[] hash, int hashOff, int hashLen,
+		byte[] sig, int sigOff, int sigLen)
+	{
+		return Verify(pk, PKCS1_ND, null,
+			hash, hashOff, hashLen,
+			sig, sigOff, sigLen);
+	}
+
+	/*
+	 * Verify a RSA signature, with PKCS#1 v1.5 "type 1" padding.
+	 * The digest header, digest alternative header, hashed data,
+	 * and signature value are provided. On any error (including
+	 * an invalid RSA public key), false is returned.
+	 *
+	 * If 'headerAlt' is null, then the signature MUST use the
+	 * header value provided in 'header'. Otherwise, the signature
+	 * MUST use either 'header' or 'headerAlt'.
+	 */
+
+	public static bool Verify(RSAPublicKey pk,
+		byte[] header, byte[] headerAlt,
+		byte[] hash, byte[] sig)
+	{
+		return Verify(pk, header, headerAlt,
+			hash, 0, hash.Length,
+			sig, 0, sig.Length);
+	}
+
+	public static bool Verify(RSAPublicKey pk,
+		byte[] header, byte[] headerAlt,
+		byte[] hash, int hashOff, int hashLen, byte[] sig)
+	{
+		return Verify(pk, header, headerAlt,
+			hash, hashOff, hashLen,
+			sig, 0, sig.Length);
+	}
+
+	public static bool Verify(RSAPublicKey pk,
+		byte[] header, byte[] headerAlt,
+		byte[] hash, byte[] sig, int sigOff, int sigLen)
+	{
+		return Verify(pk, header, headerAlt,
+			hash, 0, hash.Length,
+			sig, sigOff, sigLen);
+	}
+
+	public static bool Verify(RSAPublicKey pk,
+		byte[] header, byte[] headerAlt,
+		byte[] hash, int hashOff, int hashLen,
+		byte[] sig, int sigOff, int sigLen)
+	{
+		/*
+		 * Signature must be an integer less than the modulus,
+		 * but encoded over exactly the same size as the modulus.
+		 */
+		byte[] n = pk.Modulus;
+		int modLen = n.Length;
+		if (sigLen != modLen) {
+			return false;
+		}
+		byte[] x = new byte[modLen];
+		Array.Copy(sig, sigOff, x, 0, modLen);
+		if (BigInt.Compare(x, n) >= 0) {
+			return false;
+		}
+
+		/*
+		 * Do the RSA exponentation, then verify and remove the
+		 * "Type 1" padding (00 01 FF...FF 00 with at least
+		 * eight bytes of value FF).
+		 */
+		x = BigInt.ModPow(x, pk.Exponent, n);
+		if (x.Length < 11 || x[0] != 0x00 || x[1] != 0x01) {
+			return false;
+		}
+		int k = 2;
+		while (k < x.Length && x[k] == 0xFF) {
+			k ++;
+		}
+		if (k < 10 || k == x.Length || x[k] != 0x00) {
+			return false;
+		}
+		k ++;
+
+		/*
+		 * Check that the remaining byte end with the provided
+		 * hash value.
+		 */
+		int len = modLen - k;
+		if (len < hashLen) {
+			return false;
+		}
+		for (int i = 0; i < hashLen; i ++) {
+			if (x[modLen - hashLen + i] != hash[hashOff + i]) {
+				return false;
+			}
+		}
+		len -= hashLen;
+
+		/*
+		 * Header is at offset 'k', and length 'len'. Compare
+		 * with the provided header(s).
+		 */
+		if (Eq(header, 0, header.Length, x, k, len)) {
+			return true;
+		}
+		if (headerAlt != null) {
+			if (Eq(headerAlt, 0, headerAlt.Length, x, k, len)) {
+				return true;
+			}
+		}
+		return false;
+	}
+
+	/*
+	 * Compute a RSA signature (PKCS#1 v1.5 "type 1" padding).
+	 * The digest header and the hashed data are provided. The
+	 * header should be one of the standard PKCS#1 header; it
+	 * may also be an empty array or null for a "ND" signature
+	 * (this is normally used only in SSL/TLS up to TLS-1.1).
+	 */
+
+	public static byte[] Sign(RSAPrivateKey sk, byte[] header,
+		byte[] hash)
+	{
+		return Sign(sk, header, hash, 0, hash.Length);
+	}
+
+	public static byte[] Sign(RSAPrivateKey sk, byte[] header,
+		byte[] hash, int hashOff, int hashLen)
+	{
+		byte[] sig = new byte[sk.N.Length];
+		Sign(sk, header, hash, hashOff, hashLen, sig, 0);
+		return sig;
+	}
+
+	public static int Sign(RSAPrivateKey sk, byte[] header,
+		byte[] hash, byte[] outBuf, int outOff)
+	{
+		return Sign(sk, header, hash, 0, hash.Length, outBuf, outOff);
+	}
+
+	public static int Sign(RSAPrivateKey sk, byte[] header,
+		byte[] hash, int hashOff, int hashLen,
+		byte[] outBuf, int outOff)
+	{
+		int modLen = sk.N.Length;
+		byte[] x = DoPKCS1Padding(modLen, false,
+			header, hash, hashOff, hashLen);
+		DoPrivate(sk, x);
+		Array.Copy(x, 0, outBuf, outOff, x.Length);
+		return x.Length;
+	}
+
+	/*
+	 * Apply PKCS#1 v1.5 padding. The data to pad is the concatenation
+	 * of head[] and the chunk of val[] beginning at valOff and of
+	 * length valLen. If 'head' is null, then an empty array is used.
+	 *
+	 * The returned array has length modLen (the modulus size, in bytes).
+	 * Padding type 2 (random bytes, for encryption) is used if type2
+	 * is true; otherwise, padding type 1 is applied (bytes 0xFF, for
+	 * signatures).
+	 */
+	public static byte[] DoPKCS1Padding(int modLen, bool type2,
+		byte[] head, byte[] val, int valOff, int valLen)
+	{
+		if (head == null) {
+			head = PKCS1_ND;
+		}
+		int len = head.Length + valLen;
+		int padLen = modLen - len - 3;
+		if (padLen < 8) {
+			throw new Exception(
+				"modulus too short for PKCS#1 padding");
+		}
+		byte[] x = new byte[modLen];
+		x[0] = 0x00;
+		x[1] = (byte)(type2 ? 0x02 : 0x01);
+		if (type2) {
+			RNG.GetBytesNonZero(x, 2, padLen);
+		} else {
+			for (int i = 0; i < padLen; i ++) {
+				x[i + 2] = 0xFF;
+			}
+		}
+		x[padLen + 2] = 0x00;
+		Array.Copy(head, 0, x, padLen + 3, head.Length);
+		Array.Copy(val, valOff, x, modLen - valLen, valLen);
+		return x;
+	}
+
+	/*
+	 * Compare two byte chunks for equality.
+	 */
+	static bool Eq(byte[] a, int aoff, int alen,
+		byte[] b, int boff, int blen)
+	{
+		if (alen != blen) {
+			return false;
+		}
+		for (int i = 0; i < alen; i ++) {
+			if (a[aoff + i] != b[boff + i]) {
+				return false;
+			}
+		}
+		return true;
+	}
+}
+
+}
diff --git a/Crypto/RSAPrivateKey.cs b/Crypto/RSAPrivateKey.cs
new file mode 100644
index 0000000..fea0a1a
--- /dev/null
+++ b/Crypto/RSAPrivateKey.cs
@@ -0,0 +1,312 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * This class contains a RSA private key. The private key elements can
+ * be exported as arrays of bytes (minimal unsigned big-endian
+ * representation).
+ *
+ * Private key elements are:
+ *   n    modulus
+ *   e    public exponent
+ *   d    private exponent
+ *   p    first modulus factor
+ *   q    second modulus factor
+ *   dp   d mod (p-1)
+ *   dq   d mod (q-1)
+ *   iq   (1/q) mod p
+ */
+
+public class RSAPrivateKey : IPrivateKey {
+
+	public byte[] N {
+		get {
+			return n;
+		}
+	}
+
+	public byte[] E {
+		get {
+			return e;
+		}
+	}
+
+	public byte[] D {
+		get {
+			return d;
+		}
+	}
+
+	public byte[] P {
+		get {
+			return p;
+		}
+	}
+
+	public byte[] Q {
+		get {
+			return q;
+		}
+	}
+
+	public byte[] DP {
+		get {
+			return dp;
+		}
+	}
+
+	public byte[] DQ {
+		get {
+			return dq;
+		}
+	}
+
+	public byte[] IQ {
+		get {
+			return iq;
+		}
+	}
+
+	public int KeySizeBits {
+		get {
+			return ((n.Length - 1) << 3)
+				+ BigInt.BitLength(n[0]);
+		}
+	}
+
+	public string AlgorithmName {
+		get {
+			return "RSA";
+		}
+	}
+
+	IPublicKey IPrivateKey.PublicKey {
+		get {
+			return this.PublicKey;
+		}
+	}
+
+	public RSAPublicKey PublicKey {
+		get {
+			return new RSAPublicKey(n, e);
+		}
+	}
+
+	byte[] n, e, d, p, q, dp, dq, iq;
+
+	/*
+	 * Create a new instance with the provided elements. Values are
+	 * in unsigned big-endian representation.
+	 *
+	 *   n    modulus
+	 *   e    public exponent
+	 *   d    private exponent
+	 *   p    first modulus factor
+	 *   q    second modulus factor
+	 *   dp   d mod (p-1)
+	 *   dq   d mod (q-1)
+	 *   iq   (1/q) mod p
+	 *
+	 * Rules verified by this constructor:
+	 *   n must be odd and at least 512 bits
+	 *   e must be odd
+	 *   p must be odd
+	 *   q must be odd
+	 *   p and q are greater than 1
+	 *   n is equal to p*q
+	 *   dp must be non-zero and lower than p-1
+	 *   dq must be non-zero and lower than q-1
+	 *   iq must be non-zero and lower than p
+	 *
+	 * This constructor does NOT verify that:
+	 *   p and q are prime
+	 *   d is equal to dp modulo p-1
+	 *   d is equal to dq modulo q-1
+	 *   dp is the inverse of e modulo p-1
+	 *   dq is the inverse of e modulo q-1
+	 *   iq is the inverse of q modulo p
+	 */
+	public RSAPrivateKey(byte[] n, byte[] e, byte[] d,
+		byte[] p, byte[] q, byte[] dp, byte[] dq, byte[] iq)
+	{
+		n = BigInt.NormalizeBE(n);
+		e = BigInt.NormalizeBE(e);
+		d = BigInt.NormalizeBE(d);
+		p = BigInt.NormalizeBE(p);
+		q = BigInt.NormalizeBE(q);
+		dp = BigInt.NormalizeBE(dp);
+		dq = BigInt.NormalizeBE(dq);
+		iq = BigInt.NormalizeBE(iq);
+
+		if (n.Length < 64 || (n.Length == 64 && n[0] < 0x80)) {
+			throw new CryptoException(
+				"Invalid RSA private key (less than 512 bits)");
+		}
+		if (!BigInt.IsOdd(n)) {
+			throw new CryptoException(
+				"Invalid RSA private key (even modulus)");
+		}
+		if (!BigInt.IsOdd(e)) {
+			throw new CryptoException(
+				"Invalid RSA private key (even exponent)");
+		}
+		if (!BigInt.IsOdd(p) || !BigInt.IsOdd(q)) {
+			throw new CryptoException(
+				"Invalid RSA private key (even factor)");
+		}
+		if (BigInt.IsOne(p) || BigInt.IsOne(q)) {
+			throw new CryptoException(
+				"Invalid RSA private key (trivial factor)");
+		}
+		if (BigInt.Compare(n, BigInt.Mul(p, q)) != 0) {
+			throw new CryptoException(
+				"Invalid RSA private key (bad factors)");
+		}
+		if (dp.Length == 0 || dq.Length == 0) {
+			throw new CryptoException(
+				"Invalid RSA private key"
+				+ " (null reduced private exponent)");
+		}
+
+		/*
+		 * We can temporarily modify p[] and q[] (to compute
+		 * p-1 and q-1) since these are freshly produced copies.
+		 */
+		p[p.Length - 1] --;
+		q[q.Length - 1] --;
+		if (BigInt.Compare(dp, p) >= 0 || BigInt.Compare(dq, q) >= 0) {
+			throw new CryptoException(
+				"Invalid RSA private key"
+				+ " (oversized reduced private exponent)");
+		}
+		p[p.Length - 1] ++;
+		q[q.Length - 1] ++;
+		if (iq.Length == 0 || BigInt.Compare(iq, p) >= 0) {
+			throw new CryptoException(
+				"Invalid RSA private key"
+				+ " (out of range CRT coefficient)");
+		}
+		this.n = n;
+		this.e = e;
+		this.d = d;
+		this.p = p;
+		this.q = q;
+		this.dp = dp;
+		this.dq = dq;
+		this.iq = iq;
+	}
+
+	/*
+	 * Create a new instance with the provided elements: the two
+	 * factors, and the public exponent. The other elements are
+	 * computed. Values are in unsigned big-endian representation.
+	 * Rules verified by this constructor:
+	 *   p must be odd
+	 *   q must be odd
+	 *   e must be relatively prime to both p-1 and q-1
+	 *   e must be greater than 1
+	 *   p*q must have size at least 512 bits
+	 * TODO: not implemented yet.
+	 */
+	public RSAPrivateKey(byte[] p, byte[] q, byte[] e)
+	{
+		throw new Exception("NYI");
+	}
+
+	/*
+	 * Create a new instance with the provided elements: the two
+	 * factors, and the public exponent. The other elements are
+	 * computed. The factors are in unsigned big-endian
+	 * representation; the public exponent is a small integer.
+	 * Rules verified by this constructor:
+	 *   p must be odd
+	 *   q must be odd
+	 *   e must be relatively prime to both p-1 and q-1
+	 *   p*q must have size at least 512 bits
+	 * TODO: not implemented yet.
+	 */
+	public RSAPrivateKey(byte[] p, byte[] q, uint e)
+		: this(p, q, ToBytes(e))
+	{
+	}
+
+	static byte[] ToBytes(uint x)
+	{
+		byte[] r = new byte[4];
+		r[0] = (byte)(x >> 24);
+		r[1] = (byte)(x >> 16);
+		r[2] = (byte)(x >> 8);
+		r[3] = (byte)x;
+		return r;
+	}
+
+	/*
+	 * CheckValid() will verify that the prime factors are indeed
+	 * prime, and that all other values are correct.
+	 */
+	public void CheckValid()
+	{
+		/*
+		 * Factors ought to be prime.
+		 */
+		if (!BigInt.IsPrime(p) || !BigInt.IsPrime(q)) {
+			throw new CryptoException("Invalid RSA private key"
+				+ " (non-prime factor)");
+		}
+
+		/*
+		 * FIXME: Verify that:
+		 *   dp = d mod p-1
+		 *   e*dp = 1 mod p-1
+		 *   dq = d mod q-1
+		 *   e*dq = 1 mod q-1
+		 * (This is not easy with existing code because p-1 and q-1
+		 * are even, but ModInt tolerates only odd moduli.)
+		 *
+		CheckExp(p, d, dp, e);
+		CheckExp(q, d, dq, e);
+		 */
+
+		/*
+		 * Verify that:
+		 *   q*iq = 1 mod p
+		 */
+		ModInt x = new ModInt(p);
+		ModInt y = x.Dup();
+		x.DecodeReduce(q);
+		x.ToMonty();
+		y.Decode(iq);
+		x.MontyMul(y);
+		if (!x.IsOne) {
+			throw new CryptoException("Invalid RSA private key"
+				+ " (wrong CRT coefficient)");
+		}
+	}
+}
+
+}
diff --git a/Crypto/RSAPublicKey.cs b/Crypto/RSAPublicKey.cs
new file mode 100644
index 0000000..315a7a3
--- /dev/null
+++ b/Crypto/RSAPublicKey.cs
@@ -0,0 +1,142 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * This class contains a RSA public key, defined as a modulus and an
+ * exponent. Both use big-endian representation. This class normalizes
+ * the parameters provided to the constructor so that modulus and
+ * exponent use their minimal unsigned big-endian representation.
+ *
+ * Modulus must have length at least 512 bits. Modulus and exponent
+ * must be odd integers.
+ */
+
+public class RSAPublicKey : IPublicKey {
+
+	public byte[] Modulus {
+		get {
+			return mod;
+		}
+	}
+
+	public byte[] Exponent {
+		get {
+			return e;
+		}
+	}
+
+	public int KeySizeBits {
+		get {
+			return ((mod.Length - 1) << 3)
+				+ BigInt.BitLength(mod[0]);
+		}
+	}
+
+	public string AlgorithmName {
+		get {
+			return "RSA";
+		}
+	}
+
+	byte[] mod;
+	byte[] e;
+	int hashCode;
+
+	/*
+	 * Create a new instance with the provided element (unsigned,
+	 * big-endian). This constructor checks the following rules:
+	 *
+	 *   the modulus size must be at least 512 bits
+	 *   the modulus must be odd
+	 *   the exponent must be odd and greater than 1
+	 */
+	public RSAPublicKey(byte[] modulus, byte[] exponent)
+	{
+		mod = BigInt.NormalizeBE(modulus, false);
+		e = BigInt.NormalizeBE(exponent, false);
+		if (mod.Length < 64 || (mod.Length == 64 && mod[0] < 0x80)) {
+			throw new CryptoException(
+				"Invalid RSA public key (less than 512 bits)");
+		}
+		if ((mod[mod.Length - 1] & 0x01) == 0) {
+			throw new CryptoException(
+				"Invalid RSA public key (even modulus)");
+		}
+		if (BigInt.IsZero(e)) {
+			throw new CryptoException(
+				"Invalid RSA public key (exponent is zero)");
+		}
+		if (BigInt.IsOne(e)) {
+			throw new CryptoException(
+				"Invalid RSA public key (exponent is one)");
+		}
+		if ((e[e.Length - 1] & 0x01) == 0) {
+			throw new CryptoException(
+				"Invalid RSA public key (even exponent)");
+		}
+
+		/*
+		 * A simple hash code that will work well because RSA
+		 * keys are in practice quite randomish.
+		 */
+		hashCode = (int)(BigInt.HashInt(modulus)
+			^ BigInt.HashInt(exponent));
+	}
+
+	/*
+	 * For a RSA public key, we cannot, in all generality, check
+	 * any more things than we already did in the constructor.
+	 * Notably, we cannot check whether the public exponent (e)
+	 * is indeed relatively prime to phi(n) (the order of the
+	 * invertible group modulo n).
+	 */
+	public void CheckValid()
+	{
+		/*
+		 * We cannot check more than what we already checked in
+		 * the constructor.
+		 */
+	}
+
+	public override bool Equals(object obj)
+	{
+		RSAPublicKey p = obj as RSAPublicKey;
+		if (p == null) {
+			return false;
+		}
+		return BigInt.Compare(mod, p.mod) == 0
+			&& BigInt.Compare(e, p.e) == 0;
+	}
+
+	public override int GetHashCode()
+	{
+		return hashCode;
+	}
+}
+
+}
diff --git a/Crypto/SHA1.cs b/Crypto/SHA1.cs
new file mode 100644
index 0000000..7ba7bce
--- /dev/null
+++ b/Crypto/SHA1.cs
@@ -0,0 +1,559 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * SHA-1 implementation. SHA-1 is described in FIPS 180-4. Note that
+ * SHA-1 collisions can be computed more efficiently than what would be
+ * expected from an ideal hash function with the same output size; and
+ * this was actually done at least once. Use with care.
+ */
+
+public sealed class SHA1 : DigestCore {
+
+	const int BLOCK_LEN = 64;
+
+	uint A, B, C, D, E;
+	byte[] block, saveBlock;
+	int ptr;
+	ulong byteCount;
+
+	/* 
+	 * Create a new instance. It is ready to process data bytes.
+	 */
+	public SHA1()
+	{
+		block = new byte[BLOCK_LEN];
+		saveBlock = new byte[BLOCK_LEN];
+		Reset();
+	}
+
+	/* see IDigest */
+	public override string Name {
+		get {
+			return "SHA-1";
+		}
+	}
+
+	/* see IDigest */
+	public override int DigestSize {
+		get {
+			return 20;
+		}
+	}
+
+	/* see IDigest */
+	public override int BlockSize {
+		get {
+			return 64;
+		}
+	}
+
+	/* see IDigest */
+	public override void Reset()
+	{
+		A = 0x67452301;
+		B = 0xEFCDAB89;
+		C = 0x98BADCFE;
+		D = 0x10325476;
+		E = 0xC3D2E1F0;
+		byteCount = 0;
+		ptr = 0;
+	}
+
+	/* see IDigest */
+	public override void Update(byte b)
+	{
+		block[ptr ++] = b;
+		byteCount ++;
+		if (ptr == BLOCK_LEN) {
+			ProcessBlock();
+		}
+	}
+
+	/* see IDigest */
+	public override void Update(byte[] buf, int off, int len)
+	{
+		if (len < 0) {
+			throw new ArgumentException("negative chunk length");
+		}
+		byteCount += (ulong)len;
+		while (len > 0) {
+			int clen = Math.Min(len, BLOCK_LEN - ptr);
+			Array.Copy(buf, off, block, ptr, clen);
+			off += clen;
+			len -= clen;
+			ptr += clen;
+			if (ptr == BLOCK_LEN) {
+				ProcessBlock();
+			}
+		}
+	}
+
+	/* see IDigest */
+	public override void DoPartial(byte[] outBuf, int off)
+	{
+		/*
+		 * Save current state.
+		 */
+		uint saveA = A;
+		uint saveB = B;
+		uint saveC = C;
+		uint saveD = D;
+		uint saveE = E;
+		int savePtr = ptr;
+		Array.Copy(block, 0, saveBlock, 0, savePtr);
+
+		/*
+		 * Add padding. This may involve processing an extra block.
+		 */
+		block[ptr ++] = 0x80;
+		if (ptr > BLOCK_LEN - 8) {
+			for (int j = ptr; j < BLOCK_LEN; j ++) {
+				block[j] = 0;
+			}
+			ProcessBlock();
+		}
+		for (int j = ptr; j < (BLOCK_LEN - 8); j ++) {
+			block[j] = 0;
+		}
+		ulong x = byteCount << 3;
+		Enc32be((uint)(x >> 32), block, BLOCK_LEN - 8);
+		Enc32be((uint)x, block, BLOCK_LEN - 4);
+
+		/*
+		 * Process final block and encode result.
+		 */
+		ProcessBlock();
+		Enc32be(A, outBuf, off);
+		Enc32be(B, outBuf, off + 4);
+		Enc32be(C, outBuf, off + 8);
+		Enc32be(D, outBuf, off + 12);
+		Enc32be(E, outBuf, off + 16);
+
+		/*
+		 * Restore current state.
+		 */
+		Array.Copy(saveBlock, 0, block, 0, savePtr);
+		A = saveA;
+		B = saveB;
+		C = saveC;
+		D = saveD;
+		E = saveE;
+		ptr = savePtr;
+	}
+
+	/* see IDigest */
+	public override IDigest Dup()
+	{
+		SHA1 h = new SHA1();
+		h.A = A;
+		h.B = B;
+		h.C = C;
+		h.D = D;
+		h.E = E;
+		h.ptr = ptr;
+		h.byteCount = byteCount;
+		Array.Copy(block, 0, h.block, 0, ptr);
+		return h;
+	}
+
+	/* see IDigest */
+	public override void CurrentState(byte[] outBuf, int off)
+	{
+		Enc32be(A, outBuf, off);
+		Enc32be(B, outBuf, off + 4);
+		Enc32be(C, outBuf, off + 8);
+		Enc32be(D, outBuf, off + 12);
+		Enc32be(E, outBuf, off + 16);
+	}
+
+	const uint K1 = 0x5A827999;
+	const uint K2 = 0x6ED9EBA1;
+	const uint K3 = 0x8F1BBCDC;
+	const uint K4 = 0xCA62C1D6;
+
+	void ProcessBlock()
+	{
+		/*
+		 * Read state words.
+		 */
+		uint wa = A;
+		uint wb = B;
+		uint wc = C;
+		uint wd = D;
+		uint we = E;
+
+		/*
+		 * Rounds 0 to 19.
+		 */
+		uint x0 = Dec32be(block,  0);
+		we += ((wa << 5) | (wa >> 27)) + (wd ^ (wb & (wc ^ wd))) + x0 + K1;
+		wb = (wb << 30) | (wb >> 2);
+		uint x1 = Dec32be(block,  4);
+		wd += ((we << 5) | (we >> 27)) + (wc ^ (wa & (wb ^ wc))) + x1 + K1;
+		wa = (wa << 30) | (wa >> 2);
+		uint x2 = Dec32be(block,  8);
+		wc += ((wd << 5) | (wd >> 27)) + (wb ^ (we & (wa ^ wb))) + x2 + K1;
+		we = (we << 30) | (we >> 2);
+		uint x3 = Dec32be(block, 12);
+		wb += ((wc << 5) | (wc >> 27)) + (wa ^ (wd & (we ^ wa))) + x3 + K1;
+		wd = (wd << 30) | (wd >> 2);
+		uint x4 = Dec32be(block, 16);
+		wa += ((wb << 5) | (wb >> 27)) + (we ^ (wc & (wd ^ we))) + x4 + K1;
+		wc = (wc << 30) | (wc >> 2);
+		uint x5 = Dec32be(block, 20);
+		we += ((wa << 5) | (wa >> 27)) + (wd ^ (wb & (wc ^ wd))) + x5 + K1;
+		wb = (wb << 30) | (wb >> 2);
+		uint x6 = Dec32be(block, 24);
+		wd += ((we << 5) | (we >> 27)) + (wc ^ (wa & (wb ^ wc))) + x6 + K1;
+		wa = (wa << 30) | (wa >> 2);
+		uint x7 = Dec32be(block, 28);
+		wc += ((wd << 5) | (wd >> 27)) + (wb ^ (we & (wa ^ wb))) + x7 + K1;
+		we = (we << 30) | (we >> 2);
+		uint x8 = Dec32be(block, 32);
+		wb += ((wc << 5) | (wc >> 27)) + (wa ^ (wd & (we ^ wa))) + x8 + K1;
+		wd = (wd << 30) | (wd >> 2);
+		uint x9 = Dec32be(block, 36);
+		wa += ((wb << 5) | (wb >> 27)) + (we ^ (wc & (wd ^ we))) + x9 + K1;
+		wc = (wc << 30) | (wc >> 2);
+		uint xA = Dec32be(block, 40);
+		we += ((wa << 5) | (wa >> 27)) + (wd ^ (wb & (wc ^ wd))) + xA + K1;
+		wb = (wb << 30) | (wb >> 2);
+		uint xB = Dec32be(block, 44);
+		wd += ((we << 5) | (we >> 27)) + (wc ^ (wa & (wb ^ wc))) + xB + K1;
+		wa = (wa << 30) | (wa >> 2);
+		uint xC = Dec32be(block, 48);
+		wc += ((wd << 5) | (wd >> 27)) + (wb ^ (we & (wa ^ wb))) + xC + K1;
+		we = (we << 30) | (we >> 2);
+		uint xD = Dec32be(block, 52);
+		wb += ((wc << 5) | (wc >> 27)) + (wa ^ (wd & (we ^ wa))) + xD + K1;
+		wd = (wd << 30) | (wd >> 2);
+		uint xE = Dec32be(block, 56);
+		wa += ((wb << 5) | (wb >> 27)) + (we ^ (wc & (wd ^ we))) + xE + K1;
+		wc = (wc << 30) | (wc >> 2);
+		uint xF = Dec32be(block, 60);
+		we += ((wa << 5) | (wa >> 27)) + (wd ^ (wb & (wc ^ wd))) + xF + K1;
+		wb = (wb << 30) | (wb >> 2);
+		x0 ^= xD ^ x8 ^ x2;
+		x0 = (x0 << 1) | (x0 >> 31);
+		wd += ((we << 5) | (we >> 27)) + (wc ^ (wa & (wb ^ wc))) + x0 + K1;
+		wa = (wa << 30) | (wa >> 2);
+		x1 ^= xE ^ x9 ^ x3;
+		x1 = (x1 << 1) | (x1 >> 31);
+		wc += ((wd << 5) | (wd >> 27)) + (wb ^ (we & (wa ^ wb))) + x1 + K1;
+		we = (we << 30) | (we >> 2);
+		x2 ^= xF ^ xA ^ x4;
+		x2 = (x2 << 1) | (x2 >> 31);
+		wb += ((wc << 5) | (wc >> 27)) + (wa ^ (wd & (we ^ wa))) + x2 + K1;
+		wd = (wd << 30) | (wd >> 2);
+		x3 ^= x0 ^ xB ^ x5;
+		x3 = (x3 << 1) | (x3 >> 31);
+		wa += ((wb << 5) | (wb >> 27)) + (we ^ (wc & (wd ^ we))) + x3 + K1;
+		wc = (wc << 30) | (wc >> 2);
+
+		/*
+		 * Rounds 20 to 39.
+		 */
+		x4 ^= x1 ^ xC ^ x6;
+		x4 = (x4 << 1) | (x4 >> 31);
+		we += ((wa << 5) | (wa >> 27)) + (wb ^ wc ^ wd) + x4 + K2;
+		wb = (wb << 30) | (wb >> 2);
+		x5 ^= x2 ^ xD ^ x7;
+		x5 = (x5 << 1) | (x5 >> 31);
+		wd += ((we << 5) | (we >> 27)) + (wa ^ wb ^ wc) + x5 + K2;
+		wa = (wa << 30) | (wa >> 2);
+		x6 ^= x3 ^ xE ^ x8;
+		x6 = (x6 << 1) | (x6 >> 31);
+		wc += ((wd << 5) | (wd >> 27)) + (we ^ wa ^ wb) + x6 + K2;
+		we = (we << 30) | (we >> 2);
+		x7 ^= x4 ^ xF ^ x9;
+		x7 = (x7 << 1) | (x7 >> 31);
+		wb += ((wc << 5) | (wc >> 27)) + (wd ^ we ^ wa) + x7 + K2;
+		wd = (wd << 30) | (wd >> 2);
+		x8 ^= x5 ^ x0 ^ xA;
+		x8 = (x8 << 1) | (x8 >> 31);
+		wa += ((wb << 5) | (wb >> 27)) + (wc ^ wd ^ we) + x8 + K2;
+		wc = (wc << 30) | (wc >> 2);
+		x9 ^= x6 ^ x1 ^ xB;
+		x9 = (x9 << 1) | (x9 >> 31);
+		we += ((wa << 5) | (wa >> 27)) + (wb ^ wc ^ wd) + x9 + K2;
+		wb = (wb << 30) | (wb >> 2);
+		xA ^= x7 ^ x2 ^ xC;
+		xA = (xA << 1) | (xA >> 31);
+		wd += ((we << 5) | (we >> 27)) + (wa ^ wb ^ wc) + xA + K2;
+		wa = (wa << 30) | (wa >> 2);
+		xB ^= x8 ^ x3 ^ xD;
+		xB = (xB << 1) | (xB >> 31);
+		wc += ((wd << 5) | (wd >> 27)) + (we ^ wa ^ wb) + xB + K2;
+		we = (we << 30) | (we >> 2);
+		xC ^= x9 ^ x4 ^ xE;
+		xC = (xC << 1) | (xC >> 31);
+		wb += ((wc << 5) | (wc >> 27)) + (wd ^ we ^ wa) + xC + K2;
+		wd = (wd << 30) | (wd >> 2);
+		xD ^= xA ^ x5 ^ xF;
+		xD = (xD << 1) | (xD >> 31);
+		wa += ((wb << 5) | (wb >> 27)) + (wc ^ wd ^ we) + xD + K2;
+		wc = (wc << 30) | (wc >> 2);
+		xE ^= xB ^ x6 ^ x0;
+		xE = (xE << 1) | (xE >> 31);
+		we += ((wa << 5) | (wa >> 27)) + (wb ^ wc ^ wd) + xE + K2;
+		wb = (wb << 30) | (wb >> 2);
+		xF ^= xC ^ x7 ^ x1;
+		xF = (xF << 1) | (xF >> 31);
+		wd += ((we << 5) | (we >> 27)) + (wa ^ wb ^ wc) + xF + K2;
+		wa = (wa << 30) | (wa >> 2);
+		x0 ^= xD ^ x8 ^ x2;
+		x0 = (x0 << 1) | (x0 >> 31);
+		wc += ((wd << 5) | (wd >> 27)) + (we ^ wa ^ wb) + x0 + K2;
+		we = (we << 30) | (we >> 2);
+		x1 ^= xE ^ x9 ^ x3;
+		x1 = (x1 << 1) | (x1 >> 31);
+		wb += ((wc << 5) | (wc >> 27)) + (wd ^ we ^ wa) + x1 + K2;
+		wd = (wd << 30) | (wd >> 2);
+		x2 ^= xF ^ xA ^ x4;
+		x2 = (x2 << 1) | (x2 >> 31);
+		wa += ((wb << 5) | (wb >> 27)) + (wc ^ wd ^ we) + x2 + K2;
+		wc = (wc << 30) | (wc >> 2);
+		x3 ^= x0 ^ xB ^ x5;
+		x3 = (x3 << 1) | (x3 >> 31);
+		we += ((wa << 5) | (wa >> 27)) + (wb ^ wc ^ wd) + x3 + K2;
+		wb = (wb << 30) | (wb >> 2);
+		x4 ^= x1 ^ xC ^ x6;
+		x4 = (x4 << 1) | (x4 >> 31);
+		wd += ((we << 5) | (we >> 27)) + (wa ^ wb ^ wc) + x4 + K2;
+		wa = (wa << 30) | (wa >> 2);
+		x5 ^= x2 ^ xD ^ x7;
+		x5 = (x5 << 1) | (x5 >> 31);
+		wc += ((wd << 5) | (wd >> 27)) + (we ^ wa ^ wb) + x5 + K2;
+		we = (we << 30) | (we >> 2);
+		x6 ^= x3 ^ xE ^ x8;
+		x6 = (x6 << 1) | (x6 >> 31);
+		wb += ((wc << 5) | (wc >> 27)) + (wd ^ we ^ wa) + x6 + K2;
+		wd = (wd << 30) | (wd >> 2);
+		x7 ^= x4 ^ xF ^ x9;
+		x7 = (x7 << 1) | (x7 >> 31);
+		wa += ((wb << 5) | (wb >> 27)) + (wc ^ wd ^ we) + x7 + K2;
+		wc = (wc << 30) | (wc >> 2);
+
+		/*
+		 * Rounds 40 to 59.
+		 */
+		x8 ^= x5 ^ x0 ^ xA;
+		x8 = (x8 << 1) | (x8 >> 31);
+		we += ((wa << 5) | (wa >> 27)) + ((wc & wd) ^ (wb & (wc ^ wd))) + x8 + K3;
+		wb = (wb << 30) | (wb >> 2);
+		x9 ^= x6 ^ x1 ^ xB;
+		x9 = (x9 << 1) | (x9 >> 31);
+		wd += ((we << 5) | (we >> 27)) + ((wb & wc) ^ (wa & (wb ^ wc))) + x9 + K3;
+		wa = (wa << 30) | (wa >> 2);
+		xA ^= x7 ^ x2 ^ xC;
+		xA = (xA << 1) | (xA >> 31);
+		wc += ((wd << 5) | (wd >> 27)) + ((wa & wb) ^ (we & (wa ^ wb))) + xA + K3;
+		we = (we << 30) | (we >> 2);
+		xB ^= x8 ^ x3 ^ xD;
+		xB = (xB << 1) | (xB >> 31);
+		wb += ((wc << 5) | (wc >> 27)) + ((we & wa) ^ (wd & (we ^ wa))) + xB + K3;
+		wd = (wd << 30) | (wd >> 2);
+		xC ^= x9 ^ x4 ^ xE;
+		xC = (xC << 1) | (xC >> 31);
+		wa += ((wb << 5) | (wb >> 27)) + ((wd & we) ^ (wc & (wd ^ we))) + xC + K3;
+		wc = (wc << 30) | (wc >> 2);
+		xD ^= xA ^ x5 ^ xF;
+		xD = (xD << 1) | (xD >> 31);
+		we += ((wa << 5) | (wa >> 27)) + ((wc & wd) ^ (wb & (wc ^ wd))) + xD + K3;
+		wb = (wb << 30) | (wb >> 2);
+		xE ^= xB ^ x6 ^ x0;
+		xE = (xE << 1) | (xE >> 31);
+		wd += ((we << 5) | (we >> 27)) + ((wb & wc) ^ (wa & (wb ^ wc))) + xE + K3;
+		wa = (wa << 30) | (wa >> 2);
+		xF ^= xC ^ x7 ^ x1;
+		xF = (xF << 1) | (xF >> 31);
+		wc += ((wd << 5) | (wd >> 27)) + ((wa & wb) ^ (we & (wa ^ wb))) + xF + K3;
+		we = (we << 30) | (we >> 2);
+		x0 ^= xD ^ x8 ^ x2;
+		x0 = (x0 << 1) | (x0 >> 31);
+		wb += ((wc << 5) | (wc >> 27)) + ((we & wa) ^ (wd & (we ^ wa))) + x0 + K3;
+		wd = (wd << 30) | (wd >> 2);
+		x1 ^= xE ^ x9 ^ x3;
+		x1 = (x1 << 1) | (x1 >> 31);
+		wa += ((wb << 5) | (wb >> 27)) + ((wd & we) ^ (wc & (wd ^ we))) + x1 + K3;
+		wc = (wc << 30) | (wc >> 2);
+		x2 ^= xF ^ xA ^ x4;
+		x2 = (x2 << 1) | (x2 >> 31);
+		we += ((wa << 5) | (wa >> 27)) + ((wc & wd) ^ (wb & (wc ^ wd))) + x2 + K3;
+		wb = (wb << 30) | (wb >> 2);
+		x3 ^= x0 ^ xB ^ x5;
+		x3 = (x3 << 1) | (x3 >> 31);
+		wd += ((we << 5) | (we >> 27)) + ((wb & wc) ^ (wa & (wb ^ wc))) + x3 + K3;
+		wa = (wa << 30) | (wa >> 2);
+		x4 ^= x1 ^ xC ^ x6;
+		x4 = (x4 << 1) | (x4 >> 31);
+		wc += ((wd << 5) | (wd >> 27)) + ((wa & wb) ^ (we & (wa ^ wb))) + x4 + K3;
+		we = (we << 30) | (we >> 2);
+		x5 ^= x2 ^ xD ^ x7;
+		x5 = (x5 << 1) | (x5 >> 31);
+		wb += ((wc << 5) | (wc >> 27)) + ((we & wa) ^ (wd & (we ^ wa))) + x5 + K3;
+		wd = (wd << 30) | (wd >> 2);
+		x6 ^= x3 ^ xE ^ x8;
+		x6 = (x6 << 1) | (x6 >> 31);
+		wa += ((wb << 5) | (wb >> 27)) + ((wd & we) ^ (wc & (wd ^ we))) + x6 + K3;
+		wc = (wc << 30) | (wc >> 2);
+		x7 ^= x4 ^ xF ^ x9;
+		x7 = (x7 << 1) | (x7 >> 31);
+		we += ((wa << 5) | (wa >> 27)) + ((wc & wd) ^ (wb & (wc ^ wd))) + x7 + K3;
+		wb = (wb << 30) | (wb >> 2);
+		x8 ^= x5 ^ x0 ^ xA;
+		x8 = (x8 << 1) | (x8 >> 31);
+		wd += ((we << 5) | (we >> 27)) + ((wb & wc) ^ (wa & (wb ^ wc))) + x8 + K3;
+		wa = (wa << 30) | (wa >> 2);
+		x9 ^= x6 ^ x1 ^ xB;
+		x9 = (x9 << 1) | (x9 >> 31);
+		wc += ((wd << 5) | (wd >> 27)) + ((wa & wb) ^ (we & (wa ^ wb))) + x9 + K3;
+		we = (we << 30) | (we >> 2);
+		xA ^= x7 ^ x2 ^ xC;
+		xA = (xA << 1) | (xA >> 31);
+		wb += ((wc << 5) | (wc >> 27)) + ((we & wa) ^ (wd & (we ^ wa))) + xA + K3;
+		wd = (wd << 30) | (wd >> 2);
+		xB ^= x8 ^ x3 ^ xD;
+		xB = (xB << 1) | (xB >> 31);
+		wa += ((wb << 5) | (wb >> 27)) + ((wd & we) ^ (wc & (wd ^ we))) + xB + K3;
+		wc = (wc << 30) | (wc >> 2);
+
+		/*
+		 * Rounds 60 to 79.
+		 */
+		xC ^= x9 ^ x4 ^ xE;
+		xC = (xC << 1) | (xC >> 31);
+		we += ((wa << 5) | (wa >> 27)) + (wb ^ wc ^ wd) + xC + K4;
+		wb = (wb << 30) | (wb >> 2);
+		xD ^= xA ^ x5 ^ xF;
+		xD = (xD << 1) | (xD >> 31);
+		wd += ((we << 5) | (we >> 27)) + (wa ^ wb ^ wc) + xD + K4;
+		wa = (wa << 30) | (wa >> 2);
+		xE ^= xB ^ x6 ^ x0;
+		xE = (xE << 1) | (xE >> 31);
+		wc += ((wd << 5) | (wd >> 27)) + (we ^ wa ^ wb) + xE + K4;
+		we = (we << 30) | (we >> 2);
+		xF ^= xC ^ x7 ^ x1;
+		xF = (xF << 1) | (xF >> 31);
+		wb += ((wc << 5) | (wc >> 27)) + (wd ^ we ^ wa) + xF + K4;
+		wd = (wd << 30) | (wd >> 2);
+		x0 ^= xD ^ x8 ^ x2;
+		x0 = (x0 << 1) | (x0 >> 31);
+		wa += ((wb << 5) | (wb >> 27)) + (wc ^ wd ^ we) + x0 + K4;
+		wc = (wc << 30) | (wc >> 2);
+		x1 ^= xE ^ x9 ^ x3;
+		x1 = (x1 << 1) | (x1 >> 31);
+		we += ((wa << 5) | (wa >> 27)) + (wb ^ wc ^ wd) + x1 + K4;
+		wb = (wb << 30) | (wb >> 2);
+		x2 ^= xF ^ xA ^ x4;
+		x2 = (x2 << 1) | (x2 >> 31);
+		wd += ((we << 5) | (we >> 27)) + (wa ^ wb ^ wc) + x2 + K4;
+		wa = (wa << 30) | (wa >> 2);
+		x3 ^= x0 ^ xB ^ x5;
+		x3 = (x3 << 1) | (x3 >> 31);
+		wc += ((wd << 5) | (wd >> 27)) + (we ^ wa ^ wb) + x3 + K4;
+		we = (we << 30) | (we >> 2);
+		x4 ^= x1 ^ xC ^ x6;
+		x4 = (x4 << 1) | (x4 >> 31);
+		wb += ((wc << 5) | (wc >> 27)) + (wd ^ we ^ wa) + x4 + K4;
+		wd = (wd << 30) | (wd >> 2);
+		x5 ^= x2 ^ xD ^ x7;
+		x5 = (x5 << 1) | (x5 >> 31);
+		wa += ((wb << 5) | (wb >> 27)) + (wc ^ wd ^ we) + x5 + K4;
+		wc = (wc << 30) | (wc >> 2);
+		x6 ^= x3 ^ xE ^ x8;
+		x6 = (x6 << 1) | (x6 >> 31);
+		we += ((wa << 5) | (wa >> 27)) + (wb ^ wc ^ wd) + x6 + K4;
+		wb = (wb << 30) | (wb >> 2);
+		x7 ^= x4 ^ xF ^ x9;
+		x7 = (x7 << 1) | (x7 >> 31);
+		wd += ((we << 5) | (we >> 27)) + (wa ^ wb ^ wc) + x7 + K4;
+		wa = (wa << 30) | (wa >> 2);
+		x8 ^= x5 ^ x0 ^ xA;
+		x8 = (x8 << 1) | (x8 >> 31);
+		wc += ((wd << 5) | (wd >> 27)) + (we ^ wa ^ wb) + x8 + K4;
+		we = (we << 30) | (we >> 2);
+		x9 ^= x6 ^ x1 ^ xB;
+		x9 = (x9 << 1) | (x9 >> 31);
+		wb += ((wc << 5) | (wc >> 27)) + (wd ^ we ^ wa) + x9 + K4;
+		wd = (wd << 30) | (wd >> 2);
+		xA ^= x7 ^ x2 ^ xC;
+		xA = (xA << 1) | (xA >> 31);
+		wa += ((wb << 5) | (wb >> 27)) + (wc ^ wd ^ we) + xA + K4;
+		wc = (wc << 30) | (wc >> 2);
+		xB ^= x8 ^ x3 ^ xD;
+		xB = (xB << 1) | (xB >> 31);
+		we += ((wa << 5) | (wa >> 27)) + (wb ^ wc ^ wd) + xB + K4;
+		wb = (wb << 30) | (wb >> 2);
+		xC ^= x9 ^ x4 ^ xE;
+		xC = (xC << 1) | (xC >> 31);
+		wd += ((we << 5) | (we >> 27)) + (wa ^ wb ^ wc) + xC + K4;
+		wa = (wa << 30) | (wa >> 2);
+		xD ^= xA ^ x5 ^ xF;
+		xD = (xD << 1) | (xD >> 31);
+		wc += ((wd << 5) | (wd >> 27)) + (we ^ wa ^ wb) + xD + K4;
+		we = (we << 30) | (we >> 2);
+		xE ^= xB ^ x6 ^ x0;
+		xE = (xE << 1) | (xE >> 31);
+		wb += ((wc << 5) | (wc >> 27)) + (wd ^ we ^ wa) + xE + K4;
+		wd = (wd << 30) | (wd >> 2);
+		xF ^= xC ^ x7 ^ x1;
+		xF = (xF << 1) | (xF >> 31);
+		wa += ((wb << 5) | (wb >> 27)) + (wc ^ wd ^ we) + xF + K4;
+		wc = (wc << 30) | (wc >> 2);
+
+		/*
+		 * Update state words and reset block pointer.
+		 */
+		A += wa;
+		B += wb;
+		C += wc;
+		D += wd;
+		E += we;
+		ptr = 0;
+	}
+
+	static uint Dec32be(byte[] buf, int off)
+	{
+		return ((uint)buf[off] << 24)
+			| ((uint)buf[off + 1] << 16)
+			| ((uint)buf[off + 2] << 8)
+			| (uint)buf[off + 3];
+	}
+
+	static void Enc32be(uint x, byte[] buf, int off)
+	{
+		buf[off] = (byte)(x >> 24);
+		buf[off + 1] = (byte)(x >> 16);
+		buf[off + 2] = (byte)(x >> 8);
+		buf[off + 3] = (byte)x;
+	}
+}
+
+}
diff --git a/Crypto/SHA224.cs b/Crypto/SHA224.cs
new file mode 100644
index 0000000..236bbd4
--- /dev/null
+++ b/Crypto/SHA224.cs
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * SHA-224 implementation. SHA-224 is described in FIPS 180-4.
+ */
+
+public sealed class SHA224 : SHA2Small {
+
+	/*
+	 * Create a new instance, ready to process data bytes.
+	 */
+	public SHA224()
+	{
+	}
+
+	/* see IDigest */
+	public override string Name {
+		get {
+			return "SHA-224";
+		}
+	}
+
+	/* see IDigest */
+	public override int DigestSize {
+		get {
+			return 28;
+		}
+	}
+
+	internal override uint[] IV {
+		get {
+			return IV224;
+		}
+	}
+
+	internal override SHA2Small DupInner()
+	{
+		return new SHA224();
+	}
+
+	static uint[] IV224 = {
+		0xC1059ED8, 0x367CD507, 0x3070DD17, 0xF70E5939,
+		0xFFC00B31, 0x68581511, 0x64F98FA7, 0xBEFA4FA4
+	};
+}
+
+}
diff --git a/Crypto/SHA256.cs b/Crypto/SHA256.cs
new file mode 100644
index 0000000..3328eb5
--- /dev/null
+++ b/Crypto/SHA256.cs
@@ -0,0 +1,73 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * SHA-256 implementation. SHA-256 is described in FIPS 180-4.
+ */
+
+public sealed class SHA256 : SHA2Small {
+
+	/*
+	 * Create a new instance, ready to process data bytes.
+	 */
+	public SHA256()
+	{
+	}
+
+	/* see IDigest */
+	public override string Name {
+		get {
+			return "SHA-256";
+		}
+	}
+
+	/* see IDigest */
+	public override int DigestSize {
+		get {
+			return 32;
+		}
+	}
+
+	internal override uint[] IV {
+		get {
+			return IV256;
+		}
+	}
+
+	internal override SHA2Small DupInner()
+	{
+		return new SHA256();
+	}
+
+	static uint[] IV256 = {
+		0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A,
+		0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19
+	};
+}
+
+}
diff --git a/Crypto/SHA2Big.cs b/Crypto/SHA2Big.cs
new file mode 100644
index 0000000..0e38ac6
--- /dev/null
+++ b/Crypto/SHA2Big.cs
@@ -0,0 +1,387 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * Implementation of SHA-384 and SHA-512, as described in FIPS 180-4.
+ */
+
+public abstract class SHA2Big : DigestCore {
+
+	const int BLOCK_LEN = 128;
+
+	ulong[] state;
+	byte[] block, saveBlock;
+	int ptr;
+	ulong byteCount;
+	ulong[] W;
+
+	/*
+	 * Create a new instance, ready to process data bytes. The
+	 * output length (in bytes) and initial value must be specified.
+	 */
+	internal SHA2Big()
+	{
+		state = new ulong[8];
+		block = new byte[BLOCK_LEN];
+		saveBlock = new byte[BLOCK_LEN];
+		W = new ulong[80];
+		Reset();
+	}
+
+	internal abstract ulong[] IV { get; }
+
+	internal abstract SHA2Big DupInner();
+
+	/* see IDigest */
+	public override int BlockSize {
+		get {
+			return BLOCK_LEN;
+		}
+	}
+
+	/* see IDigest */
+	public override void Reset()
+	{
+		Array.Copy(IV, 0, state, 0, state.Length);
+		byteCount = 0;
+		ptr = 0;
+	}
+
+	/* see IDigest */
+	public override void Update(byte b)
+	{
+		block[ptr ++] = b;
+		byteCount ++;
+		if (ptr == BLOCK_LEN) {
+			ProcessBlock();
+		}
+	}
+
+	/* see IDigest */
+	public override void Update(byte[] buf, int off, int len)
+	{
+		if (len < 0) {
+			throw new ArgumentException("negative chunk length");
+		}
+		byteCount += (ulong)len;
+		while (len > 0) {
+			int clen = Math.Min(len, BLOCK_LEN - ptr);
+			Array.Copy(buf, off, block, ptr, clen);
+			off += clen;
+			len -= clen;
+			ptr += clen;
+			if (ptr == BLOCK_LEN) {
+				ProcessBlock();
+			}
+		}
+	}
+
+	/* see IDigest */
+	public override void DoPartial(byte[] outBuf, int off)
+	{
+		/*
+		 * Save current state.
+		 */
+		ulong A = state[0];
+		ulong B = state[1];
+		ulong C = state[2];
+		ulong D = state[3];
+		ulong E = state[4];
+		ulong F = state[5];
+		ulong G = state[6];
+		ulong H = state[7];
+		int savePtr = ptr;
+		Array.Copy(block, 0, saveBlock, 0, savePtr);
+
+		/*
+		 * Add padding. This may involve processing an extra block.
+		 */
+		block[ptr ++] = 0x80;
+		if (ptr > BLOCK_LEN - 16) {
+			for (int j = ptr; j < BLOCK_LEN; j ++) {
+				block[j] = 0;
+			}
+			ProcessBlock();
+		}
+		for (int j = ptr; j < (BLOCK_LEN - 16); j ++) {
+			block[j] = 0;
+		}
+		Enc64be(byteCount >> 61, block, BLOCK_LEN - 16);
+		Enc64be(byteCount << 3, block, BLOCK_LEN - 8);
+
+		/*
+		 * Process final block and encode result.
+		 */
+		ProcessBlock();
+		int n = DigestSize >> 3;
+		for (int i = 0; i < n; i ++) {
+			Enc64be(state[i], outBuf, off + (i << 3));
+		}
+
+		/*
+		 * Restore current state.
+		 */
+		Array.Copy(saveBlock, 0, block, 0, savePtr);
+		state[0] = A;
+		state[1] = B;
+		state[2] = C;
+		state[3] = D;
+		state[4] = E;
+		state[5] = F;
+		state[6] = G;
+		state[7] = H;
+		ptr = savePtr;
+	}
+
+	/* see IDigest */
+	public override IDigest Dup()
+	{
+		SHA2Big h = DupInner();
+		Array.Copy(state, 0, h.state, 0, state.Length);
+		h.ptr = ptr;
+		h.byteCount = byteCount;
+		Array.Copy(block, 0, h.block, 0, ptr);
+		return h;
+	}
+
+	/* see IDigest */
+	public override void CurrentState(byte[] outBuf, int off)
+	{
+		int n = DigestSize >> 3;
+		for (int i = 0; i < n; i ++) {
+			Enc64be(state[i], outBuf, off + (i << 3));
+		}
+	}
+
+	static ulong[] K = {
+		0x428A2F98D728AE22, 0x7137449123EF65CD,
+		0xB5C0FBCFEC4D3B2F, 0xE9B5DBA58189DBBC,
+		0x3956C25BF348B538, 0x59F111F1B605D019,
+		0x923F82A4AF194F9B, 0xAB1C5ED5DA6D8118,
+		0xD807AA98A3030242, 0x12835B0145706FBE,
+		0x243185BE4EE4B28C, 0x550C7DC3D5FFB4E2,
+		0x72BE5D74F27B896F, 0x80DEB1FE3B1696B1,
+		0x9BDC06A725C71235, 0xC19BF174CF692694,
+		0xE49B69C19EF14AD2, 0xEFBE4786384F25E3,
+		0x0FC19DC68B8CD5B5, 0x240CA1CC77AC9C65,
+		0x2DE92C6F592B0275, 0x4A7484AA6EA6E483,
+		0x5CB0A9DCBD41FBD4, 0x76F988DA831153B5,
+		0x983E5152EE66DFAB, 0xA831C66D2DB43210,
+		0xB00327C898FB213F, 0xBF597FC7BEEF0EE4,
+		0xC6E00BF33DA88FC2, 0xD5A79147930AA725,
+		0x06CA6351E003826F, 0x142929670A0E6E70,
+		0x27B70A8546D22FFC, 0x2E1B21385C26C926,
+		0x4D2C6DFC5AC42AED, 0x53380D139D95B3DF,
+		0x650A73548BAF63DE, 0x766A0ABB3C77B2A8,
+		0x81C2C92E47EDAEE6, 0x92722C851482353B,
+		0xA2BFE8A14CF10364, 0xA81A664BBC423001,
+		0xC24B8B70D0F89791, 0xC76C51A30654BE30,
+		0xD192E819D6EF5218, 0xD69906245565A910,
+		0xF40E35855771202A, 0x106AA07032BBD1B8,
+		0x19A4C116B8D2D0C8, 0x1E376C085141AB53,
+		0x2748774CDF8EEB99, 0x34B0BCB5E19B48A8,
+		0x391C0CB3C5C95A63, 0x4ED8AA4AE3418ACB,
+		0x5B9CCA4F7763E373, 0x682E6FF3D6B2B8A3,
+		0x748F82EE5DEFB2FC, 0x78A5636F43172F60,
+		0x84C87814A1F0AB72, 0x8CC702081A6439EC,
+		0x90BEFFFA23631E28, 0xA4506CEBDE82BDE9,
+		0xBEF9A3F7B2C67915, 0xC67178F2E372532B,
+		0xCA273ECEEA26619C, 0xD186B8C721C0C207,
+		0xEADA7DD6CDE0EB1E, 0xF57D4F7FEE6ED178,
+		0x06F067AA72176FBA, 0x0A637DC5A2C898A6,
+		0x113F9804BEF90DAE, 0x1B710B35131C471B,
+		0x28DB77F523047D84, 0x32CAAB7B40C72493,
+		0x3C9EBE0A15C9BEBC, 0x431D67C49C100D4C,
+		0x4CC5D4BECB3E42B6, 0x597F299CFC657E2A,
+		0x5FCB6FAB3AD6FAEC, 0x6C44198C4A475817
+	};
+
+	void ProcessBlock()
+	{
+		/*
+		 * Read state words.
+		 */
+		ulong A = state[0];
+		ulong B = state[1];
+		ulong C = state[2];
+		ulong D = state[3];
+		ulong E = state[4];
+		ulong F = state[5];
+		ulong G = state[6];
+		ulong H = state[7];
+
+		ulong T1, T2;
+		ulong[] W = this.W;
+		byte[] block = this.block;
+
+		for (int i = 0, j = 0; i < 16; i ++, j += 8) {
+			W[i] = Dec64be(block, j);
+		}
+		for (int i = 16; i < 80; i ++) {
+			ulong w2 = W[i - 2];
+			ulong w15 = W[i - 15];
+			W[i] = (((w2 << 45) | (w2 >> 19))
+				^ ((w2 << 3) | (w2 >> 61))
+				^ (w2 >> 6))
+				+ W[i - 7]
+				+ (((w15 << 63) | (w15 >> 1))
+				^ ((w15 << 56) | (w15 >> 8))
+				^ (w15 >> 7))
+				+ W[i - 16];
+		}
+		for (int i = 0; i < 80; i += 8) {
+			T1 = H + (((E << 50) | (E >> 14))
+				^ ((E << 46) | (E >> 18))
+				^ ((E << 23) | (E >> 41)))
+				+ (G ^ (E & (F ^ G)))
+				+ K[i + 0] + W[i + 0];
+			T2 = (((A << 36) | (A >> 28))
+				^ ((A << 30) | (A >> 34))
+				^ ((A << 25) | (A >> 39)))
+				+ ((A & B) ^ (C & (A ^ B)));
+			D += T1;
+			H = T1 + T2;
+			T1 = G + (((D << 50) | (D >> 14))
+				^ ((D << 46) | (D >> 18))
+				^ ((D << 23) | (D >> 41)))
+				+ (F ^ (D & (E ^ F)))
+				+ K[i + 1] + W[i + 1];
+			T2 = (((H << 36) | (H >> 28))
+				^ ((H << 30) | (H >> 34))
+				^ ((H << 25) | (H >> 39)))
+				+ ((H & A) ^ (B & (H ^ A)));
+			C += T1;
+			G = T1 + T2;
+			T1 = F + (((C << 50) | (C >> 14))
+				^ ((C << 46) | (C >> 18))
+				^ ((C << 23) | (C >> 41)))
+				+ (E ^ (C & (D ^ E)))
+				+ K[i + 2] + W[i + 2];
+			T2 = (((G << 36) | (G >> 28))
+				^ ((G << 30) | (G >> 34))
+				^ ((G << 25) | (G >> 39)))
+				+ ((G & H) ^ (A & (G ^ H)));
+			B += T1;
+			F = T1 + T2;
+			T1 = E + (((B << 50) | (B >> 14))
+				^ ((B << 46) | (B >> 18))
+				^ ((B << 23) | (B >> 41)))
+				+ (D ^ (B & (C ^ D)))
+				+ K[i + 3] + W[i + 3];
+			T2 = (((F << 36) | (F >> 28))
+				^ ((F << 30) | (F >> 34))
+				^ ((F << 25) | (F >> 39)))
+				+ ((F & G) ^ (H & (F ^ G)));
+			A += T1;
+			E = T1 + T2;
+			T1 = D + (((A << 50) | (A >> 14))
+				^ ((A << 46) | (A >> 18))
+				^ ((A << 23) | (A >> 41)))
+				+ (C ^ (A & (B ^ C)))
+				+ K[i + 4] + W[i + 4];
+			T2 = (((E << 36) | (E >> 28))
+				^ ((E << 30) | (E >> 34))
+				^ ((E << 25) | (E >> 39)))
+				+ ((E & F) ^ (G & (E ^ F)));
+			H += T1;
+			D = T1 + T2;
+			T1 = C + (((H << 50) | (H >> 14))
+				^ ((H << 46) | (H >> 18))
+				^ ((H << 23) | (H >> 41)))
+				+ (B ^ (H & (A ^ B)))
+				+ K[i + 5] + W[i + 5];
+			T2 = (((D << 36) | (D >> 28))
+				^ ((D << 30) | (D >> 34))
+				^ ((D << 25) | (D >> 39)))
+				+ ((D & E) ^ (F & (D ^ E)));
+			G += T1;
+			C = T1 + T2;
+			T1 = B + (((G << 50) | (G >> 14))
+				^ ((G << 46) | (G >> 18))
+				^ ((G << 23) | (G >> 41)))
+				+ (A ^ (G & (H ^ A)))
+				+ K[i + 6] + W[i + 6];
+			T2 = (((C << 36) | (C >> 28))
+				^ ((C << 30) | (C >> 34))
+				^ ((C << 25) | (C >> 39)))
+				+ ((C & D) ^ (E & (C ^ D)));
+			F += T1;
+			B = T1 + T2;
+			T1 = A + (((F << 50) | (F >> 14))
+				^ ((F << 46) | (F >> 18))
+				^ ((F << 23) | (F >> 41)))
+				+ (H ^ (F & (G ^ H)))
+				+ K[i + 7] + W[i + 7];
+			T2 = (((B << 36) | (B >> 28))
+				^ ((B << 30) | (B >> 34))
+				^ ((B << 25) | (B >> 39)))
+				+ ((B & C) ^ (D & (B ^ C)));
+			E += T1;
+			A = T1 + T2;
+		}
+
+		/*
+		 * Update state words and reset block pointer.
+		 */
+		state[0] += A;
+		state[1] += B;
+		state[2] += C;
+		state[3] += D;
+		state[4] += E;
+		state[5] += F;
+		state[6] += G;
+		state[7] += H;
+		ptr = 0;
+	}
+
+	static ulong Dec64be(byte[] buf, int off)
+	{
+		return ((ulong)buf[off] << 56)
+			| ((ulong)buf[off + 1] << 48)
+			| ((ulong)buf[off + 2] << 40)
+			| ((ulong)buf[off + 3] << 32)
+			| ((ulong)buf[off + 4] << 24)
+			| ((ulong)buf[off + 5] << 16)
+			| ((ulong)buf[off + 6] << 8)
+			| (ulong)buf[off + 7];
+	}
+
+	static void Enc64be(ulong x, byte[] buf, int off)
+	{
+		buf[off] = (byte)(x >> 56);
+		buf[off + 1] = (byte)(x >> 48);
+		buf[off + 2] = (byte)(x >> 40);
+		buf[off + 3] = (byte)(x >> 32);
+		buf[off + 4] = (byte)(x >> 24);
+		buf[off + 5] = (byte)(x >> 16);
+		buf[off + 6] = (byte)(x >> 8);
+		buf[off + 7] = (byte)x;
+	}
+}
+
+}
diff --git a/Crypto/SHA2Small.cs b/Crypto/SHA2Small.cs
new file mode 100644
index 0000000..fed41f4
--- /dev/null
+++ b/Crypto/SHA2Small.cs
@@ -0,0 +1,372 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * Implementation of SHA-224 and SHA-256, as described in FIPS 180-4.
+ */
+
+public abstract class SHA2Small : DigestCore {
+
+	const int BLOCK_LEN = 64;
+
+	uint[] state;
+	byte[] block, saveBlock;
+	int ptr;
+	ulong byteCount;
+	uint[] W;
+
+	/*
+	 * Create a new instance, ready to process data bytes. The
+	 * output length (in bytes) and initial value must be specified.
+	 */
+	internal SHA2Small()
+	{
+		state = new uint[8];
+		block = new byte[BLOCK_LEN];
+		saveBlock = new byte[BLOCK_LEN];
+		W = new uint[64];
+		Reset();
+	}
+
+	internal abstract uint[] IV { get; }
+
+	internal abstract SHA2Small DupInner();
+
+	/* see IDigest */
+	public override int BlockSize {
+		get {
+			return BLOCK_LEN;
+		}
+	}
+
+	/* see IDigest */
+	public override void Reset()
+	{
+		Array.Copy(IV, 0, state, 0, state.Length);
+		byteCount = 0;
+		ptr = 0;
+	}
+
+	/* see IDigest */
+	public override void Update(byte b)
+	{
+		block[ptr ++] = b;
+		byteCount ++;
+		if (ptr == BLOCK_LEN) {
+			ProcessBlock();
+		}
+	}
+
+	/* see IDigest */
+	public override void Update(byte[] buf, int off, int len)
+	{
+		if (len < 0) {
+			throw new ArgumentException("negative chunk length");
+		}
+		byteCount += (ulong)len;
+		while (len > 0) {
+			int clen = Math.Min(len, BLOCK_LEN - ptr);
+			Array.Copy(buf, off, block, ptr, clen);
+			off += clen;
+			len -= clen;
+			ptr += clen;
+			if (ptr == BLOCK_LEN) {
+				ProcessBlock();
+			}
+		}
+	}
+
+	/* see IDigest */
+	public override void DoPartial(byte[] outBuf, int off)
+	{
+		/*
+		 * Save current state.
+		 */
+		uint A = state[0];
+		uint B = state[1];
+		uint C = state[2];
+		uint D = state[3];
+		uint E = state[4];
+		uint F = state[5];
+		uint G = state[6];
+		uint H = state[7];
+		int savePtr = ptr;
+		Array.Copy(block, 0, saveBlock, 0, savePtr);
+
+		/*
+		 * Add padding. This may involve processing an extra block.
+		 */
+		block[ptr ++] = 0x80;
+		if (ptr > BLOCK_LEN - 8) {
+			for (int j = ptr; j < BLOCK_LEN; j ++) {
+				block[j] = 0;
+			}
+			ProcessBlock();
+		}
+		for (int j = ptr; j < (BLOCK_LEN - 8); j ++) {
+			block[j] = 0;
+		}
+		ulong x = byteCount << 3;
+		Enc32be((uint)(x >> 32), block, BLOCK_LEN - 8);
+		Enc32be((uint)x, block, BLOCK_LEN - 4);
+
+		/*
+		 * Process final block and encode result.
+		 */
+		ProcessBlock();
+		int n = DigestSize >> 2;
+		for (int i = 0; i < n; i ++) {
+			Enc32be(state[i], outBuf, off + (i << 2));
+		}
+
+		/*
+		 * Restore current state.
+		 */
+		Array.Copy(saveBlock, 0, block, 0, savePtr);
+		state[0] = A;
+		state[1] = B;
+		state[2] = C;
+		state[3] = D;
+		state[4] = E;
+		state[5] = F;
+		state[6] = G;
+		state[7] = H;
+		ptr = savePtr;
+	}
+
+	/* see IDigest */
+	public override IDigest Dup()
+	{
+		SHA2Small h = DupInner();
+		Array.Copy(state, 0, h.state, 0, state.Length);
+		h.ptr = ptr;
+		h.byteCount = byteCount;
+		Array.Copy(block, 0, h.block, 0, ptr);
+		return h;
+	}
+
+	/* see IDigest */
+	public override void CurrentState(byte[] outBuf, int off)
+	{
+		int n = DigestSize >> 2;
+		for (int i = 0; i < n; i ++) {
+			Enc32be(state[i], outBuf, off + (i << 2));
+		}
+	}
+
+	static uint[] K = {
+		0x428A2F98, 0x71374491, 0xB5C0FBCF, 0xE9B5DBA5,
+		0x3956C25B, 0x59F111F1, 0x923F82A4, 0xAB1C5ED5,
+		0xD807AA98, 0x12835B01, 0x243185BE, 0x550C7DC3,
+		0x72BE5D74, 0x80DEB1FE, 0x9BDC06A7, 0xC19BF174,
+		0xE49B69C1, 0xEFBE4786, 0x0FC19DC6, 0x240CA1CC,
+		0x2DE92C6F, 0x4A7484AA, 0x5CB0A9DC, 0x76F988DA,
+		0x983E5152, 0xA831C66D, 0xB00327C8, 0xBF597FC7,
+		0xC6E00BF3, 0xD5A79147, 0x06CA6351, 0x14292967,
+		0x27B70A85, 0x2E1B2138, 0x4D2C6DFC, 0x53380D13,
+		0x650A7354, 0x766A0ABB, 0x81C2C92E, 0x92722C85,
+		0xA2BFE8A1, 0xA81A664B, 0xC24B8B70, 0xC76C51A3,
+		0xD192E819, 0xD6990624, 0xF40E3585, 0x106AA070,
+		0x19A4C116, 0x1E376C08, 0x2748774C, 0x34B0BCB5,
+		0x391C0CB3, 0x4ED8AA4A, 0x5B9CCA4F, 0x682E6FF3,
+		0x748F82EE, 0x78A5636F, 0x84C87814, 0x8CC70208,
+		0x90BEFFFA, 0xA4506CEB, 0xBEF9A3F7, 0xC67178F2
+	};
+
+	void ProcessBlock()
+	{
+		/*
+		 * Read state words.
+		 */
+		uint A = state[0];
+		uint B = state[1];
+		uint C = state[2];
+		uint D = state[3];
+		uint E = state[4];
+		uint F = state[5];
+		uint G = state[6];
+		uint H = state[7];
+
+		uint T1, T2;
+		uint[] W = this.W;
+		byte[] block = this.block;
+
+		for (int i = 0, j = 0; i < 16; i ++, j += 4) {
+			W[i] = Dec32be(block, j);
+		}
+		for (int i = 16; i < 64; i ++) {
+			uint w2 = W[i - 2];
+			uint w15 = W[i - 15];
+			W[i] = (((w2 << 15) | (w2 >> 17))
+				^ ((w2 << 13) | (w2 >> 19))
+				^ (w2 >> 10))
+				+ W[i - 7]
+				+ (((w15 << 25) | (w15 >> 7))
+				^ ((w15 << 14) | (w15 >> 18))
+				^ (w15 >> 3))
+				+ W[i - 16];
+		}
+		for (int i = 0; i < 64; i += 8) {
+			T1 = H + (((E << 26) | (E >> 6))
+				^ ((E << 21) | (E >> 11))
+				^ ((E << 7) | (E >> 25)))
+				+ (G ^ (E & (F ^ G)))
+				+ K[i + 0] + W[i + 0];
+			T2 = (((A << 30) | (A >> 2))
+				^ ((A << 19) | (A >> 13))
+				^ ((A << 10) | (A >> 22)))
+				+ ((A & B) ^ (C & (A ^ B)));
+			D += T1;
+			H = T1 + T2;
+			T1 = G + (((D << 26) | (D >> 6))
+				^ ((D << 21) | (D >> 11))
+				^ ((D << 7) | (D >> 25)))
+				+ (F ^ (D & (E ^ F)))
+				+ K[i + 1] + W[i + 1];
+			T2 = (((H << 30) | (H >> 2))
+				^ ((H << 19) | (H >> 13))
+				^ ((H << 10) | (H >> 22)))
+				+ ((H & A) ^ (B & (H ^ A)));
+			C += T1;
+			G = T1 + T2;
+			T1 = F + (((C << 26) | (C >> 6))
+				^ ((C << 21) | (C >> 11))
+				^ ((C << 7) | (C >> 25)))
+				+ (E ^ (C & (D ^ E)))
+				+ K[i + 2] + W[i + 2];
+			T2 = (((G << 30) | (G >> 2))
+				^ ((G << 19) | (G >> 13))
+				^ ((G << 10) | (G >> 22)))
+				+ ((G & H) ^ (A & (G ^ H)));
+			B += T1;
+			F = T1 + T2;
+			T1 = E + (((B << 26) | (B >> 6))
+				^ ((B << 21) | (B >> 11))
+				^ ((B << 7) | (B >> 25)))
+				+ (D ^ (B & (C ^ D)))
+				+ K[i + 3] + W[i + 3];
+			T2 = (((F << 30) | (F >> 2))
+				^ ((F << 19) | (F >> 13))
+				^ ((F << 10) | (F >> 22)))
+				+ ((F & G) ^ (H & (F ^ G)));
+			A += T1;
+			E = T1 + T2;
+			T1 = D + (((A << 26) | (A >> 6))
+				^ ((A << 21) | (A >> 11))
+				^ ((A << 7) | (A >> 25)))
+				+ (C ^ (A & (B ^ C)))
+				+ K[i + 4] + W[i + 4];
+			T2 = (((E << 30) | (E >> 2))
+				^ ((E << 19) | (E >> 13))
+				^ ((E << 10) | (E >> 22)))
+				+ ((E & F) ^ (G & (E ^ F)));
+			H += T1;
+			D = T1 + T2;
+			T1 = C + (((H << 26) | (H >> 6))
+				^ ((H << 21) | (H >> 11))
+				^ ((H << 7) | (H >> 25)))
+				+ (B ^ (H & (A ^ B)))
+				+ K[i + 5] + W[i + 5];
+			T2 = (((D << 30) | (D >> 2))
+				^ ((D << 19) | (D >> 13))
+				^ ((D << 10) | (D >> 22)))
+				+ ((D & E) ^ (F & (D ^ E)));
+			G += T1;
+			C = T1 + T2;
+			T1 = B + (((G << 26) | (G >> 6))
+				^ ((G << 21) | (G >> 11))
+				^ ((G << 7) | (G >> 25)))
+				+ (A ^ (G & (H ^ A)))
+				+ K[i + 6] + W[i + 6];
+			T2 = (((C << 30) | (C >> 2))
+				^ ((C << 19) | (C >> 13))
+				^ ((C << 10) | (C >> 22)))
+				+ ((C & D) ^ (E & (C ^ D)));
+			F += T1;
+			B = T1 + T2;
+			T1 = A + (((F << 26) | (F >> 6))
+				^ ((F << 21) | (F >> 11))
+				^ ((F << 7) | (F >> 25)))
+				+ (H ^ (F & (G ^ H)))
+				+ K[i + 7] + W[i + 7];
+			T2 = (((B << 30) | (B >> 2))
+				^ ((B << 19) | (B >> 13))
+				^ ((B << 10) | (B >> 22)))
+				+ ((B & C) ^ (D & (B ^ C)));
+			E += T1;
+			A = T1 + T2;
+		}
+
+		/* obsolete
+		for (int i = 0; i < 64; i ++) {
+			uint T1 = H + (((E << 26) | (E >> 6))
+				^ ((E << 21) | (E >> 11))
+				^ ((E << 7) | (E >> 25)))
+				+ (G ^ (E & (F ^ G)))
+				+ K[i] + W[i];
+			uint T2 = (((A << 30) | (A >> 2))
+				^ ((A << 19) | (A >> 13))
+				^ ((A << 10) | (A >> 22)))
+				+ ((A & B) ^ (C & (A ^ B)));
+			H = G; G = F; F = E; E = D + T1;
+			D = C; C = B; B = A; A = T1 + T2;
+		}
+		*/
+
+		/*
+		 * Update state words and reset block pointer.
+		 */
+		state[0] += A;
+		state[1] += B;
+		state[2] += C;
+		state[3] += D;
+		state[4] += E;
+		state[5] += F;
+		state[6] += G;
+		state[7] += H;
+		ptr = 0;
+	}
+
+	static uint Dec32be(byte[] buf, int off)
+	{
+		return ((uint)buf[off] << 24)
+			| ((uint)buf[off + 1] << 16)
+			| ((uint)buf[off + 2] << 8)
+			| (uint)buf[off + 3];
+	}
+
+	static void Enc32be(uint x, byte[] buf, int off)
+	{
+		buf[off] = (byte)(x >> 24);
+		buf[off + 1] = (byte)(x >> 16);
+		buf[off + 2] = (byte)(x >> 8);
+		buf[off + 3] = (byte)x;
+	}
+}
+
+}
diff --git a/Crypto/SHA384.cs b/Crypto/SHA384.cs
new file mode 100644
index 0000000..27eac33
--- /dev/null
+++ b/Crypto/SHA384.cs
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * SHA-384 implementation. SHA-384 is described in FIPS 180-4.
+ */
+
+public sealed class SHA384 : SHA2Big {
+
+	/*
+	 * Create a new instance, ready to process data bytes.
+	 */
+	public SHA384()
+	{
+	}
+
+	/* see IDigest */
+	public override string Name {
+		get {
+			return "SHA-384";
+		}
+	}
+
+	/* see IDigest */
+	public override int DigestSize {
+		get {
+			return 48;
+		}
+	}
+
+	internal override ulong[] IV {
+		get {
+			return IV384;
+		}
+	}
+
+	internal override SHA2Big DupInner()
+	{
+		return new SHA384();
+	}
+
+	static ulong[] IV384 = {
+		0xCBBB9D5DC1059ED8, 0x629A292A367CD507,
+		0x9159015A3070DD17, 0x152FECD8F70E5939,
+		0x67332667FFC00B31, 0x8EB44A8768581511,
+		0xDB0C2E0D64F98FA7, 0x47B5481DBEFA4FA4
+	};
+}
+
+}
diff --git a/Crypto/SHA512.cs b/Crypto/SHA512.cs
new file mode 100644
index 0000000..b5fb22b
--- /dev/null
+++ b/Crypto/SHA512.cs
@@ -0,0 +1,75 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace Crypto {
+
+/*
+ * SHA-512 implementation. SHA-512 is described in FIPS 180-4.
+ */
+
+public sealed class SHA512 : SHA2Big {
+
+	/*
+	 * Create a new instance, ready to process data bytes.
+	 */
+	public SHA512()
+	{
+	}
+
+	/* see IDigest */
+	public override string Name {
+		get {
+			return "SHA-512";
+		}
+	}
+
+	/* see IDigest */
+	public override int DigestSize {
+		get {
+			return 64;
+		}
+	}
+
+	internal override ulong[] IV {
+		get {
+			return IV512;
+		}
+	}
+
+	internal override SHA2Big DupInner()
+	{
+		return new SHA512();
+	}
+
+	static ulong[] IV512 = {
+		0x6A09E667F3BCC908, 0xBB67AE8584CAA73B,
+		0x3C6EF372FE94F82B, 0xA54FF53A5F1D36F1,
+		0x510E527FADE682D1, 0x9B05688C2B3E6C1F,
+		0x1F83D9ABFB41BD6B, 0x5BE0CD19137E2179
+	};
+}
+
+}
diff --git a/SSLTLS/IO.cs b/SSLTLS/IO.cs
new file mode 100644
index 0000000..82b8331
--- /dev/null
+++ b/SSLTLS/IO.cs
@@ -0,0 +1,195 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.IO;
+
+namespace SSLTLS {
+
+internal class IO {
+
+	internal static void Enc16be(int x, byte[] buf, int off)
+	{
+		buf[off + 0] = (byte)(x >> 8);
+		buf[off + 1] = (byte)x;
+	}
+
+	internal static void Enc24be(int x, byte[] buf, int off)
+	{
+		buf[off + 0] = (byte)(x >> 16);
+		buf[off + 1] = (byte)(x >> 8);
+		buf[off + 2] = (byte)x;
+	}
+
+	internal static void Enc32be(uint x, byte[] buf, int off)
+	{
+		buf[off + 0] = (byte)(x >> 24);
+		buf[off + 1] = (byte)(x >> 16);
+		buf[off + 2] = (byte)(x >> 8);
+		buf[off + 3] = (byte)x;
+	}
+
+	internal static void Enc64be(ulong x, byte[] buf, int off)
+	{
+		for (int i = 0; i < 8; i ++) {
+			buf[off + 7 - i] = (byte)x;
+			x >>= 8;
+		}
+	}
+
+	internal static int Dec16be(byte[] buf, int off)
+	{
+		return (buf[off + 0] << 8)
+			| buf[off + 1];
+	}
+
+	internal static int Dec24be(byte[] buf, int off)
+	{
+		return (buf[off + 0] << 16)
+			| (buf[off + 1] << 8)
+			| buf[off + 2];
+	}
+
+	internal static uint Dec32be(byte[] buf, int off)
+	{
+		return ((uint)buf[off + 0] << 24)
+			| ((uint)buf[off + 1] << 16)
+			| ((uint)buf[off + 2] << 8)
+			| (uint)buf[off + 3];
+	}
+
+	internal static ulong Dec64be(byte[] buf, int off)
+	{
+		ulong x = 0;
+		for (int i = 0; i < 8; i ++) {
+			x = (x << 8) | buf[off + i];
+		}
+		return x;
+	}
+
+	internal static void Write16(Stream s, int x)
+	{
+		s.WriteByte((byte)(x >> 8));
+		s.WriteByte((byte)x);
+	}
+
+	internal static void Write24(Stream s, int x)
+	{
+		s.WriteByte((byte)(x >> 16));
+		s.WriteByte((byte)(x >> 8));
+		s.WriteByte((byte)x);
+	}
+
+	/*
+	 * Write a 5-byte record header at the specified offset.
+	 */
+	internal static void WriteHeader(int recordType, int version,
+		int length, byte[] buf, int off)
+	{
+		buf[off] = (byte)recordType;
+		IO.Enc16be(version, buf, off + 1);
+		IO.Enc16be(length, buf, off + 3);
+	}
+
+	/*
+	 * Read all requested bytes. Fail on unexpected EOF, unless
+	 * 'eof' is true, in which case an EOF is acceptable at the
+	 * very beginning (i.e. no byte read at all).
+	 *
+	 * Returned value is true, unless there was an EOF at the
+	 * start and 'eof' is true, in which case returned value is
+	 * false.
+	 */
+	internal static bool ReadAll(Stream s, byte[] buf, bool eof)
+	{
+		return ReadAll(s, buf, 0, buf.Length, eof);
+	}
+
+	/*
+	 * Read all requested bytes. Fail on unexpected EOF, unless
+	 * 'eof' is true, in which case an EOF is acceptable at the
+	 * very beginning (i.e. no byte read at all).
+	 *
+	 * Returned value is true, unless there was an EOF at the
+	 * start and 'eof' is true, in which case returned value is
+	 * false.
+	 */
+	internal static bool ReadAll(Stream s,
+		byte[] buf, int off, int len, bool eof)
+	{
+		int tlen = 0;
+		while (tlen < len) {
+			int rlen = s.Read(buf, off + tlen, len - tlen);
+			if (rlen <= 0) {
+				if (eof && tlen == 0) {
+					return false;
+				}
+				throw new SSLException("Unexpected EOF");
+			}
+			tlen += rlen;
+		}
+		return true;
+	}
+
+	/*
+	 * Compare two arrays of bytes for equality.
+	 */
+	internal static bool Eq(byte[] a, byte[] b)
+	{
+		return EqCT(a, b) != 0;
+	}
+
+	/*
+	 * Compare two arrays of bytes for equality. This is constant-time
+	 * if both arrays have the same length. Returned value is 0xFFFFFFFF
+	 * on equality, 0 otherwise.
+	 */
+	internal static uint EqCT(byte[] a, byte[] b)
+	{
+		int n = a.Length;
+		if (n != b.Length) {
+			return 0;
+		}
+		int z = 0;
+		for (int i = 0; i < n; i ++) {
+			z |= a[i] ^ b[i];
+		}
+		return ~(uint)((z | -z) >> 31);
+	}
+
+	/*
+	 * Duplicate an array of bytes. null is duplicated into null.
+	 */
+	internal static byte[] CopyBlob(byte[] x)
+	{
+		if (x == null) {
+			return null;
+		}
+		byte[] y = new byte[x.Length];
+		Array.Copy(x, 0, y, 0, x.Length);
+		return y;
+	}
+}
+
+}
diff --git a/SSLTLS/IServerChoices.cs b/SSLTLS/IServerChoices.cs
new file mode 100644
index 0000000..cf130e7
--- /dev/null
+++ b/SSLTLS/IServerChoices.cs
@@ -0,0 +1,66 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.IO;
+
+namespace SSLTLS {
+
+/*
+ * An IServerChoices instance represents the server policy choices for
+ * a given connection. It returns the certificate chain and cipher suite
+ * to send to the client, and it computes private key operations.
+ */
+
+public interface IServerChoices {
+
+	/*
+	 * Get the selected cipher suite.
+	 */
+	int GetCipherSuite();
+
+	/*
+	 * Get the certificate chain to send to the client.
+	 */
+	byte[][] GetCertificateChain();
+
+	/*
+	 * Compute the key exchange, based on the value sent by the
+	 * client. Returned value is the premaster secret. This method
+	 * is invoked only if the selected cipher suite is of type
+	 * RSA or static ECDH.
+	 */
+	byte[] DoKeyExchange(byte[] cke);
+
+	/*
+	 * Compute the signature on the provided ServerKeyExchange
+	 * message. The 'hashAlgo' and 'sigAlgo' values are set to
+	 * the symbolic values corresponding to the used signature
+	 * algorithm. This method is invoked only if the selected cipher
+	 * suite is of type ECDHE.
+	 */
+	byte[] DoSign(byte[] ske, out int hashAlgo, out int sigAlgo);
+}
+
+}
diff --git a/SSLTLS/IServerPolicy.cs b/SSLTLS/IServerPolicy.cs
new file mode 100644
index 0000000..38f914e
--- /dev/null
+++ b/SSLTLS/IServerPolicy.cs
@@ -0,0 +1,58 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.IO;
+
+namespace SSLTLS {
+
+/*
+ * An IServerPolicy instance is a callback object that selects the
+ * cipher suite and certificate chain to send to the client, and
+ * implements the private key operation (signature or key exchange).
+ */
+
+public interface IServerPolicy {
+
+	/*
+	 * Get the server policy choices for the provided connection.
+	 * In the 'server' object, the following are already set:
+	 *
+	 *   Version               Selected protocol version.
+	 *
+	 *   CommonCipherSuites    Common cipher suites.
+	 *
+	 *   ClientCurves          Elliptic curve supported by the client.
+	 *
+	 *   CommonCurves          Common supported curves.
+	 *
+	 *   ClientHashAndSign     Common hash and signature algorithms.
+	 *
+	 * Returned value is a callback object that embodies the choices
+	 * and will perform private key operations.
+	 */
+	IServerChoices Apply(SSLServer server);
+}
+
+}
diff --git a/SSLTLS/ISessionCache.cs b/SSLTLS/ISessionCache.cs
new file mode 100644
index 0000000..077e6f6
--- /dev/null
+++ b/SSLTLS/ISessionCache.cs
@@ -0,0 +1,59 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.IO;
+
+namespace SSLTLS {
+
+/*
+ * A session cache instance is able to cache and remember session
+ * parameters; this is typically used on a SSL server.
+ */
+
+public interface ISessionCache {
+
+	/*
+	 * Retrieve session parameters by session ID. If the client sent
+	 * an intended server name (SNI extension), then that name is
+	 * also provided as parameter (printable ASCII, normalised to
+	 * lowercase, no space); otherwise, that parameter is null.
+	 * Session cache implementations are free to use the server name
+	 * or not; if the client specified a target name, and the cache
+	 * returns parameters with a different, non-null name, then the
+	 * session resumption will be rejected by the engine.
+	 *
+	 * If no parameters are found for that ID (and optional server
+	 * name), then this method shall return null.
+	 */
+	SSLSessionParameters Retrieve(byte[] id, string serverName);
+
+	/*
+	 * Record new session parameters. These should be internally
+	 * indexed by their ID.
+	 */
+	void Store(SSLSessionParameters sp);
+}
+
+}
diff --git a/SSLTLS/InputRecord.cs b/SSLTLS/InputRecord.cs
new file mode 100644
index 0000000..20d3df1
--- /dev/null
+++ b/SSLTLS/InputRecord.cs
@@ -0,0 +1,187 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.IO;
+
+namespace SSLTLS {
+
+internal class InputRecord {
+
+	Stream sub;
+	byte[] buffer;
+	int recordPtr, recordEnd;
+	int recordType;
+	int recordVersion;
+	int expectedVersion;
+	RecordDecrypt rdec;
+
+	/*
+	 * Get the length (in bytes) of the data that remains to be
+	 * read from the received buffer.
+	 */
+	internal int BufferedLength {
+		get {
+			return recordEnd - recordPtr;
+		}
+	}
+
+	/*
+	 * Get the current record type (-1 if no record was read yet).
+	 */
+	internal int RecordType {
+		get {
+			return recordType;
+		}
+	}
+
+	/*
+	 * Get the current record version (-1 if no record was read yet).
+	 */
+	internal int RecordVersion {
+		get {
+			return recordVersion;
+		}
+	}
+
+	internal InputRecord(Stream sub)
+	{
+		this.sub = sub;
+		buffer = new byte[16384 + 500];
+		recordPtr = 0;
+		recordEnd = 0;
+		recordType = -1;
+		recordVersion = -1;
+		expectedVersion = -1;
+		rdec = new RecordDecryptPlain();
+	}
+
+	/*
+	 * Set the expected version. If this value is nonnegative, then
+	 * all subsequent records are expected to match this version; a
+	 * version mismatch will trigger an exception.
+	 *
+	 * If not initially set explicitly, then this value is automatically
+	 * set to the version of the first incoming record.
+	 */
+	internal void SetExpectedVersion(int expectedVersion)
+	{
+		this.expectedVersion = expectedVersion;
+	}
+
+	/*
+	 * Set the new decryption engine. This is possible only if the
+	 * end of the current record was reached.
+	 */
+	internal void SetDecryption(RecordDecrypt rdec)
+	{
+		if (recordPtr != recordEnd) {
+			throw new SSLException(
+				"Cannot switch encryption: buffered data");
+		}
+		this.rdec = rdec;
+	}
+
+	/*
+	 * Get next record. Returned value is false if EOF was reached
+	 * before obtaining the first record header byte.
+	 */
+	internal bool NextRecord()
+	{
+		if (!IO.ReadAll(sub, buffer, 0, 5, true)) {
+			return false;
+		}
+		recordType = buffer[0];
+		recordVersion = IO.Dec16be(buffer, 1);
+		int len = IO.Dec16be(buffer, 3);
+		if (expectedVersion >= 0 && expectedVersion != recordVersion) {
+			throw new SSLException(string.Format(
+				"Wrong record version: 0x{0:X4}"
+				+ " (expected: 0x{1:X4})",
+				recordVersion, expectedVersion));
+		} else {
+			if ((recordVersion >> 8) != 0x03) {
+				throw new SSLException(string.Format(
+					"Unsupported record version: 0x{0:X4}",
+					recordVersion));
+			}
+			if (expectedVersion < 0) {
+				expectedVersion = recordVersion;
+			}
+		}
+		if (!rdec.CheckLength(len)) {
+			throw new SSLException("Wrong record length: " + len);
+		}
+		IO.ReadAll(sub, buffer, 0, len, false);
+		int off = 0;
+		if (!rdec.Decrypt(recordType, recordVersion,
+			buffer, ref off, ref len))
+		{
+			throw new SSLException("Decryption failure");
+		}
+		recordPtr = off;
+		recordEnd = off + len;
+		return true;
+	}
+
+	/*
+	 * Read the next byte from the current record. -1 is returned if
+	 * the current record is finished.
+	 */
+	internal int Read()
+	{
+		if (recordPtr == recordEnd) {
+			return -1;
+		} else {
+			return buffer[recordPtr ++];
+		}
+	}
+
+	/*
+	 * Read some bytes from the current record. The number of
+	 * obtained bytes is returned; a short count (including 0)
+	 * is possible only if the end of the current record was
+	 * reached.
+	 */
+	internal int Read(byte[] buf)
+	{
+		return Read(buf, 0, buf.Length);
+	}
+
+	/*
+	 * Read some bytes from the current record. The number of
+	 * obtained bytes is returned; a short count (including 0)
+	 * is possible only if the end of the current record was
+	 * reached.
+	 */
+	internal int Read(byte[] buf, int off, int len)
+	{
+		int clen = Math.Min(len, recordEnd - recordPtr);
+		Array.Copy(buffer, recordPtr, buf, off, clen);
+		recordPtr += clen;
+		return clen;
+	}
+}
+
+}
diff --git a/SSLTLS/KeyUsage.cs b/SSLTLS/KeyUsage.cs
new file mode 100644
index 0000000..4cbd144
--- /dev/null
+++ b/SSLTLS/KeyUsage.cs
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace SSLTLS {
+
+/*
+ * Symbolic constants for acceptable usages of a public key:
+ *
+ *   EncryptOnly      public key may be used for asymmetric encryption or
+ *                    key exchange (TLS_RSA_* and TLS_ECDH_*), but not for
+ *                    signatures
+ *
+ *   SignOnly         public key may be used for signatures only
+ *                    (TLS_ECDHE_*), but not for asymmetric encryption or
+ *                    key exchange
+ *
+ *   EncryptAndSign   public key may be used for asymmetric encryption,
+ *                    key exchange and signatures
+ */
+
+public enum KeyUsage {
+	EncryptOnly,
+	SignOnly,
+	EncryptAndSign
+}
+
+}
diff --git a/SSLTLS/OutputRecord.cs b/SSLTLS/OutputRecord.cs
new file mode 100644
index 0000000..ebb81c9
--- /dev/null
+++ b/SSLTLS/OutputRecord.cs
@@ -0,0 +1,339 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.IO;
+
+namespace SSLTLS {
+
+internal class OutputRecord {
+
+	/*
+	 * Splitting modes are for debugging and tests. Note that the
+	 * automatic 1/n-1 split for CBC cipher suites in TLS 1.0 is
+	 * handled independently in RecordEncryptCBC.
+	 */
+
+	/* No split. */
+	internal const int MODE_NORMAL = 0;
+
+	/* Each record is split into two, of approximately the same size. */
+	internal const int MODE_SPLIT_HALF = 1;
+
+	/* Each record is preceded with an extra record of size 0. */
+	internal const int MODE_SPLIT_ZERO_BEFORE = 2;
+
+	/* Each record is split into two records (like SPLIT_HALF), and
+	   an extra zero-length record is added between the two halves. */
+	internal const int MODE_SPLIT_ZERO_HALF = 3;
+
+	/* The first byte of each record is separated into its own record. */
+	internal const int MODE_SPLIT_ONE_START = 4;
+
+	/* The last byte of each record is separated into its own record. */
+	internal const int MODE_SPLIT_ONE_END = 5;
+
+	/* The record is split into records of length 1 byte each. */
+	internal const int MODE_SPLIT_MULTI_ONE = 6;
+
+	/*
+	 * Spliting modes are only applied on the specified record types
+	 * (these are bit flags that can be combined).
+	 */
+	internal const int MODE_MT_CCS = 1 << SSL.CHANGE_CIPHER_SPEC;
+	internal const int MODE_MT_ALERT = 1 << SSL.ALERT;
+	internal const int MODE_MT_HANDSHAKE = 1 << SSL.HANDSHAKE;
+	internal const int MODE_MT_APPLICATION_DATA = 1 << SSL.APPLICATION_DATA;
+
+	const int MODE_MASK = 0xFFFF;
+
+	Stream sub;
+	byte[] buffer;
+	int ptr, basePtr, maxPtr;
+	int version;
+	int recordType;
+	RecordEncrypt renc;
+
+	int splitMode;
+	byte[] extra;
+
+	long countHandshake;
+	long countAppData;
+	long thresholdZeroHandshake;
+	long thresholdZeroAppData;
+	byte[] extra2;
+
+	internal OutputRecord(Stream sub)
+	{
+		this.sub = sub;
+		buffer = new byte[16384 + 500];
+		version = 0;
+		recordType = -1;
+		splitMode = MODE_NORMAL;
+		extra = null;
+		countHandshake = 0;
+		countAppData = 0;
+		thresholdZeroHandshake = 0;
+		thresholdZeroAppData = 0;
+		extra2 = new byte[500];
+		renc = new RecordEncryptPlain();
+		PrepNew();
+	}
+
+	/*
+	 * If set, then all I/O errors while writing on the underlying
+	 * stream will be converted to a generic SSLException with message
+	 * "Unexpected transport closure". This helps test code that
+	 * expects the peer to abort asynchronously, so the error may
+	 * be detected during both reading or writing.
+	 */
+	internal bool NormalizeIOError {
+		get; set;
+	}
+
+	internal void SetVersion(int version)
+	{
+		if (version != this.version) {
+			if (ptr != basePtr) {
+				FlushInner();
+			}
+			this.version = version;
+			PrepNew();
+		}
+	}
+
+	internal int RecordType {
+		get {
+			return recordType;
+		}
+		set {
+			if (value != recordType) {
+				if (ptr != basePtr) {
+					FlushInner();
+				}
+				recordType = value;
+			}
+		}
+	}
+
+	internal void SetEncryption(RecordEncrypt renc)
+	{
+		if (ptr != basePtr) {
+			FlushInner();
+		}
+		this.renc = renc;
+		PrepNew();
+	}
+
+	internal void SetSplitMode(int splitMode)
+	{
+		this.splitMode = splitMode;
+		if ((splitMode & MODE_MASK) != MODE_NORMAL && extra == null) {
+			extra = new byte[buffer.Length];
+		}
+	}
+
+	internal void SetThresholdZeroAppData(long t)
+	{
+		thresholdZeroAppData = t;
+	}
+
+	internal void SetThresholdZeroHandshake(long t)
+	{
+		thresholdZeroAppData = t;
+	}
+
+	void PrepNew()
+	{
+		int start = 0;
+		int end = buffer.Length;
+		renc.GetMaxPlaintext(ref start, ref end);
+		ptr = start;
+		basePtr = start;
+		maxPtr = end;
+	}
+
+	internal void Flush()
+	{
+		if (ptr == basePtr) {
+			return;
+		}
+		FlushInner();
+		sub.Flush();
+	}
+
+	internal void SendZeroLength(int type)
+	{
+		Flush();
+		int rt = RecordType;
+		RecordType = type;
+		FlushInner();
+		RecordType = rt;
+		sub.Flush();
+	}
+
+	void FlushInner()
+	{
+		int off = basePtr;
+		int len = ptr - basePtr;
+		if (version == 0) {
+			throw new Exception("Record version is not set");
+		}
+		int m = splitMode & MODE_MASK;
+		if (m == MODE_NORMAL || (splitMode & (1 << recordType)) == 0) {
+			EncryptAndWrite(off, len);
+		} else {
+			Array.Copy(buffer, off, extra, off, len);
+			switch (m) {
+			case MODE_SPLIT_HALF:
+			case MODE_SPLIT_ZERO_HALF:
+				int hlen = (len >> 1);
+				if (hlen > 0) {
+					EncryptAndWrite(off, hlen);
+				}
+				if (m == MODE_SPLIT_ZERO_HALF) {
+					EncryptAndWrite(off, 0);
+				}
+				Array.Copy(extra, off + hlen,
+					buffer, off, len - hlen);
+				hlen = len - hlen;
+				if (hlen > 0) {
+					EncryptAndWrite(off, hlen);
+				}
+				break;
+			case MODE_SPLIT_ZERO_BEFORE:
+				EncryptAndWrite(off, 0);
+				Array.Copy(extra, off, buffer, off, len);
+				if (len > 0) {
+					EncryptAndWrite(off, len);
+				}
+				break;
+			case MODE_SPLIT_ONE_START:
+				if (len > 0) {
+					EncryptAndWrite(off, 1);
+				}
+				if (len > 1) {
+					Array.Copy(extra, off + 1,
+						buffer, off, len - 1);
+					EncryptAndWrite(off, len - 1);
+				}
+				break;
+			case MODE_SPLIT_ONE_END:
+				if (len > 1) {
+					EncryptAndWrite(off, len - 1);
+				}
+				if (len > 0) {
+					buffer[off] = extra[off + len - 1];
+					EncryptAndWrite(off, 1);
+				}
+				break;
+			case MODE_SPLIT_MULTI_ONE:
+				for (int i = 0; i < len; i ++) {
+					buffer[off] = extra[off + i];
+					EncryptAndWrite(off, 1);
+				}
+				break;
+			default:
+				throw new SSLException(string.Format(
+					"Bad record splitting value: {0}", m));
+			}
+		}
+		PrepNew();
+	}
+
+	void EncryptAndWrite(int off, int len)
+	{
+		try {
+			EncryptAndWriteInner(off, len);
+		} catch {
+			if (NormalizeIOError) {
+				throw new SSLException(
+					"Unexpected transport closure");
+			} else {
+				throw;
+			}
+		}
+	}
+
+	void EncryptAndWriteInner(int off, int len)
+	{
+		if (recordType == SSL.HANDSHAKE) {
+			countHandshake ++;
+			if (countHandshake == thresholdZeroAppData) {
+				int start = 0;
+				int end = extra2.Length;
+				renc.GetMaxPlaintext(ref start, ref end);
+				int zoff = start;
+				int zlen = 0;
+				renc.Encrypt(SSL.APPLICATION_DATA, version,
+					extra2, ref zoff, ref zlen);
+				sub.Write(extra2, zoff, zlen);
+			}
+		} else if (recordType == SSL.APPLICATION_DATA) {
+			countAppData ++;
+			if (countAppData == thresholdZeroHandshake) {
+				int start = 0;
+				int end = extra2.Length;
+				renc.GetMaxPlaintext(ref start, ref end);
+				int zoff = start;
+				int zlen = 0;
+				renc.Encrypt(SSL.HANDSHAKE, version,
+					extra2, ref zoff, ref zlen);
+				sub.Write(extra2, zoff, zlen);
+			}
+		}
+
+		renc.Encrypt(recordType, version, buffer, ref off, ref len);
+		sub.Write(buffer, off, len);
+	}
+
+	internal void Write(byte x)
+	{
+		buffer[ptr ++] = x;
+		if (ptr == maxPtr) {
+			FlushInner();
+		}
+	}
+
+	internal void Write(byte[] data)
+	{
+		Write(data, 0, data.Length);
+	}
+
+	internal void Write(byte[] data, int off, int len)
+	{
+		while (len > 0) {
+			int clen = Math.Min(len, maxPtr - ptr);
+			Array.Copy(data, off, buffer, ptr, clen);
+			ptr += clen;
+			off += clen;
+			len -= clen;
+			if (ptr == maxPtr) {
+				FlushInner();
+			}
+		}
+	}
+}
+
+}
diff --git a/SSLTLS/PRF.cs b/SSLTLS/PRF.cs
new file mode 100644
index 0000000..db8427e
--- /dev/null
+++ b/SSLTLS/PRF.cs
@@ -0,0 +1,192 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Text;
+
+using Crypto;
+
+namespace SSLTLS {
+
+/*
+ * Implementation of the TLS PRF function. This class implements both the
+ * PRF for TLS 1.0 and 1.1 (based on MD5 and SHA-1), and the PRF for
+ * TLS 1.2 (based on a provided hash function).
+ */
+
+public sealed class PRF {
+
+	public static byte[] LABEL_MASTER_SECRET =
+		Encoding.UTF8.GetBytes("master secret");
+	public static byte[] LABEL_KEY_EXPANSION =
+		Encoding.UTF8.GetBytes("key expansion");
+	public static byte[] LABEL_CLIENT_FINISHED =
+		Encoding.UTF8.GetBytes("client finished");
+	public static byte[] LABEL_SERVER_FINISHED =
+		Encoding.UTF8.GetBytes("server finished");
+
+	HMAC hm1, hm2;
+	byte[] bufa1, bufa2;
+	byte[] bufb1, bufb2;
+
+	/*
+	 * Create a PRF instance, using both MD5 and SHA-1 (for TLS 1.0
+	 * and TLS 1.1).
+	 */
+	public PRF() : this(new MD5(), new SHA1())
+	{
+	}
+
+	/*
+	 * Create a PRF instance, using the provided hash function (for
+	 * TLS 1.2). The 'h' instance will be used internally.
+	 */
+	public PRF(IDigest h) : this(h, null)
+	{
+	}
+
+	/*
+	 * Get the "natural" output length; this is the output size,
+	 * or sum of output sizes, of the underlying hash function(s).
+	 */
+	public int NaturalOutputSize {
+		get {
+			int len = hm1.MACSize;
+			if (hm2 != null) {
+				len += hm2.MACSize;
+			}
+			return len;
+		}
+	}
+
+	PRF(IDigest h1, IDigest h2)
+	{
+		hm1 = new HMAC(h1);
+		bufa1 = new byte[hm1.MACSize];
+		bufb1 = new byte[hm1.MACSize];
+		if (h2 == null) {
+			hm2 = null;
+			bufa2 = null;
+			bufb2 = null;
+		} else {
+			hm2 = new HMAC(h2);
+			bufa2 = new byte[hm2.MACSize];
+			bufb2 = new byte[hm2.MACSize];
+		}
+	}
+
+	/*
+	 * Compute the PRF, result in outBuf[].
+	 */
+	public void GetBytes(byte[] secret, byte[] label, byte[] seed,
+		byte[] outBuf)
+	{
+		GetBytes(secret, label, seed, outBuf, 0, outBuf.Length);
+	}
+
+	/*
+	 * Compute the PRF, result in outBuf[] (at offset 'off', producing
+	 * exactly 'len' bytes).
+	 */
+	public void GetBytes(byte[] secret, byte[] label, byte[] seed,
+		byte[] outBuf, int off, int len)
+	{
+		for (int i = 0; i < len; i ++) {
+			outBuf[off + i] = 0;
+		}
+		if (hm2 == null) {
+			Phash(hm1, secret, 0, secret.Length,
+				bufa1, bufb1,
+				label, seed, outBuf, off, len);
+		} else {
+			int n = (secret.Length + 1) >> 1;
+			Phash(hm1, secret, 0, n,
+				bufa1, bufb1,
+				label, seed, outBuf, off, len);
+			Phash(hm2, secret, secret.Length - n, n,
+				bufa2, bufb2,
+				label, seed, outBuf, off, len);
+		}
+	}
+
+	/*
+	 * Compute the PRF, result is written in a newly allocated
+	 * array (of length 'outLen' bytes).
+	 */
+	public byte[] GetBytes(byte[] secret, byte[] label, byte[] seed,
+		int outLen)
+	{
+		byte[] r = new byte[outLen];
+		GetBytes(secret, label, seed, r, 0, outLen);
+		return r;
+	}
+
+	/*
+	 * This function computes Phash with the specified HMAC
+	 * engine, XORing the output with the current contents of
+	 * the outBuf[] buffer.
+	 */
+	static void Phash(HMAC hm, byte[] s, int soff, int slen,
+		byte[] bufa, byte[] bufb,
+		byte[] label, byte[] seed,
+		byte[] outBuf, int outOff, int outLen)
+	{
+		/*
+		 * Set the key for HMAC.
+		 */
+		hm.SetKey(s, soff, slen);
+
+		/*
+		 * Compute A(1) = HMAC(secret, seed).
+		 */
+		hm.Update(label);
+		hm.Update(seed);
+		hm.DoFinal(bufa, 0);
+		while (outLen > 0) {
+			/*
+			 * Next chunk: HMAC(secret, A(i) + label + seed)
+			 */
+			hm.Update(bufa);
+			hm.Update(label);
+			hm.Update(seed);
+			hm.DoFinal(bufb, 0);
+			int clen = Math.Min(hm.MACSize, outLen);
+			for (int i = 0; i < clen; i ++) {
+				outBuf[outOff ++] ^= bufb[i];
+			}
+			outLen -= clen;
+
+			/*
+			 * If we are not finished, then compute:
+			 * A(i+1) = HMAC(secret, A(i))
+			 */
+			if (outLen > 0) {
+				hm.Update(bufa);
+				hm.DoFinal(bufa, 0);
+			}
+		}
+	}
+}
+
+}
diff --git a/SSLTLS/RecordDecrypt.cs b/SSLTLS/RecordDecrypt.cs
new file mode 100644
index 0000000..a1b1008
--- /dev/null
+++ b/SSLTLS/RecordDecrypt.cs
@@ -0,0 +1,50 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Text;
+
+using Crypto;
+
+namespace SSLTLS {
+
+internal abstract class RecordDecrypt {
+
+	/*
+	 * Check whether an incoming record length is valid.
+	 */
+	internal abstract bool CheckLength(int len);
+
+	/*
+	 * Decrypt the incoming record. This method assumes that the
+	 * record length has already been tested with CheckLength().
+	 * The 'off' and 'len' values point to the record contents (not
+	 * including the header), and are adjusted to point at the
+	 * decrypted plaintext. On error, false is returned.
+	 */
+	internal abstract bool Decrypt(int recordType, int version,
+		byte[] data, ref int off, ref int len);
+}
+
+}
diff --git a/SSLTLS/RecordDecryptCBC.cs b/SSLTLS/RecordDecryptCBC.cs
new file mode 100644
index 0000000..347d062
--- /dev/null
+++ b/SSLTLS/RecordDecryptCBC.cs
@@ -0,0 +1,204 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Text;
+
+using Crypto;
+
+namespace SSLTLS {
+
+internal class RecordDecryptCBC : RecordDecrypt {
+
+	IBlockCipher bc;
+	HMAC hm;
+	byte[] iv, ivTmp;
+	bool explicitIV;
+	ulong seq;
+	byte[] tmp1, tmp2;
+
+	internal RecordDecryptCBC(IBlockCipher bc, HMAC hm, byte[] iv)
+	{
+		this.bc = bc;
+		this.hm = hm;
+		this.iv = new byte[bc.BlockSize];
+		this.ivTmp = new byte[bc.BlockSize];
+		if (iv == null) {
+			explicitIV = true;
+		} else {
+			Array.Copy(iv, 0, this.iv, 0, iv.Length);
+			explicitIV = false;
+		}
+		seq = 0;
+		tmp1 = new byte[Math.Max(13, hm.MACSize)];
+		tmp2 = new byte[Math.Max(13, hm.MACSize)];
+	}
+
+	internal override bool CheckLength(int len)
+	{
+		/*
+		 * Record length (not counting the header) must be
+		 * a multiple of the block size, and have enough room
+		 * for the MAC and the padding-length byte. With
+		 * TLS 1.1+, there must also be an explicit IV.
+		 */
+		int blen = bc.BlockSize;
+		int hlen = hm.MACSize;
+		if ((len & (blen - 1)) != 0) {
+			return false;
+		}
+		int minLen = hlen + 1;
+		int maxLen = (16384 + 256 + hlen) & ~(blen - 1);
+		if (explicitIV) {
+			minLen += blen;
+			maxLen += blen;
+		}
+		return len >= minLen && len <= maxLen;
+	}
+
+	internal override bool Decrypt(int recordType, int version,
+		byte[] data, ref int off, ref int len)
+	{
+		int blen = bc.BlockSize;
+		int hlen = hm.MACSize;
+
+		/*
+		 * Grab a copy of the last encrypted block; this is
+		 * the "saved IV" for the next record.
+		 */
+		Array.Copy(data, off + len - blen, ivTmp, 0, blen);
+
+		/*
+		 * Decrypt the data. The length has already been
+		 * checked. If there is an explicit IV, it gets
+		 * "decrypted" as well, which is not a problem.
+		 */
+		bc.CBCDecrypt(iv, data, off, len);
+		Array.Copy(ivTmp, 0, iv, 0, blen);
+		if (explicitIV) {
+			off += blen;
+			len -= blen;
+		}
+
+		/*
+		 * Compute minimum and maximum length of plaintext + MAC.
+		 * These can be inferred from the observable record length,
+		 * and thus are not secret.
+		 */
+		int minLen = (hlen + 256 < len) ? len - 256 : hlen;
+		int maxLen = len - 1;
+
+		/*
+		 * Get the actual padding length and check padding. The
+		 * padding length must match the minLen/maxLen range.
+		 */
+		int padLen = data[off + len - 1];
+		int good = ~(((maxLen - minLen) - padLen) >> 31);
+		int lenWithMAC = minLen ^ (good & (minLen ^ (maxLen - padLen)));
+		int dbb = 0;
+		for (int i = minLen; i < maxLen; i ++) {
+			dbb |= ~((i - lenWithMAC) >> 31)
+				& (data[off + i] ^ padLen);
+		}
+		good &= ~((dbb | -dbb) >> 31);
+
+		/*
+		 * Extract the MAC value; this is done in one pass, but
+		 * results in a "rotate" MAC value. The rotation count
+		 * is kept in 'rotCount': this is the offset of the
+		 * first MAC value byte in tmp1[].
+		 */
+		int lenNoMAC = lenWithMAC - hlen;
+		minLen -= hlen;
+		int rotCount = 0;
+		for (int i = 0; i < hlen; i ++) {
+			tmp1[i] = 0;
+		}
+		int v = 0;
+		for (int i = minLen; i < maxLen; i ++) {
+			int m = ~((i - lenNoMAC) >> 31)
+				& ((i - lenWithMAC) >> 31);
+			tmp1[v] |= (byte)(m & data[off + i]);
+			m = i - lenNoMAC;
+			rotCount |= ~((m | -m) >> 31) & v;
+			if (++ v == hlen) {
+				v = 0;
+			}
+		}
+		maxLen -= hlen;
+
+		/*
+		 * Rotate back the MAC value. We do it bit by bit, with
+		 * 6 iterations; this is good for all MAC value up to
+		 * and including 64 bytes.
+		 */
+		for (int i = 5; i >= 0; i --) {
+			int rc = 1 << i;
+			if (rc >= hlen) {
+				continue;
+			}
+			int ctl = -((rotCount >> i) & 1);
+			for (int j = 0, k = rc; j < hlen; j ++) {
+				int b1 = tmp1[j];
+				int b2 = tmp1[k];
+				tmp2[j] = (byte)(b1 ^ (ctl & (b1 ^ b2)));
+				if (++ k == hlen) {
+					k = 0;
+				}
+			}
+			Array.Copy(tmp2, 0, tmp1, 0, hlen);
+			rotCount &= ~rc;
+		}
+
+		/*
+		 * Recompute the HMAC value. At that point, minLen and
+		 * maxLen have been adjusted to match the plaintext
+		 * without the MAC.
+		 */
+		IO.Enc64be(seq ++, tmp2, 0);
+		IO.WriteHeader(recordType, version, lenNoMAC, tmp2, 8);
+		hm.Update(tmp2, 0, 13);
+		hm.ComputeCT(data, off, lenNoMAC, minLen, maxLen, tmp2, 0);
+
+		/*
+		 * Compare MAC values.
+		 */
+		dbb = 0;
+		for (int i = 0; i < hlen; i ++) {
+			dbb |= tmp1[i] ^ tmp2[i];
+		}
+		good &= ~((dbb | -dbb) >> 31);
+
+		/*
+		 * We must also check that the plaintext length fits in
+		 * the maximum allowed by the standard (previous check
+		 * was on the encrypted length).
+		 */
+		good &= (lenNoMAC - 16385) >> 31;
+		len = lenNoMAC;
+		return good != 0;
+	}
+}
+
+}
diff --git a/SSLTLS/RecordDecryptChaPol.cs b/SSLTLS/RecordDecryptChaPol.cs
new file mode 100644
index 0000000..13be67e
--- /dev/null
+++ b/SSLTLS/RecordDecryptChaPol.cs
@@ -0,0 +1,100 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Text;
+
+using Crypto;
+
+namespace SSLTLS {
+
+internal class RecordDecryptChaPol : RecordDecrypt {
+
+	Poly1305 pp;
+	byte[] iv;
+	byte[] nonce;
+	byte[] tmp;
+	byte[] tag;
+	ulong seq;
+
+	internal RecordDecryptChaPol(Poly1305 pp, byte[] iv)
+	{
+		this.pp = pp;
+		this.iv = new byte[12];
+		Array.Copy(iv, 0, this.iv, 0, 12);
+		nonce = new byte[12];
+		tmp = new byte[13];
+		tag = new byte[16];
+		seq = 0;
+	}
+
+	internal override bool CheckLength(int len)
+	{
+		return len >= 16 && len <= (16384 + 16);
+	}
+
+	internal override bool Decrypt(int recordType, int version,
+		byte[] data, ref int off, ref int len)
+	{
+		/*
+		 * Make the "additional data" for the MAC:
+		 *  -- sequence number (8 bytes, big-endian)
+		 *  -- header with plaintext length (5 bytes)
+		 */
+		len -= 16;
+		IO.Enc64be(seq, tmp, 0);
+		IO.WriteHeader(recordType, version, len, tmp, 8);
+
+		/*
+		 * The ChaCha20+Poly1305 IV consists in the
+		 * implicit IV (12 bytes), with the sequence number
+		 * "XORed" in the last 8 bytes (big-endian).
+		 */
+		Array.Copy(iv, 0, nonce, 0, 12);
+		for (int i = 0; i < 8; i ++) {
+			nonce[i + 4] ^= tmp[i];
+		}
+
+		/*
+		 * Do encryption and compute tag.
+		 */
+		pp.Run(nonce, data, off, len, tmp, 0, 13, tag, false);
+
+		/*
+		 * Each record has its own sequence number.
+		 */
+		seq ++;
+
+		/*
+		 * Compare the tag value.
+		 */
+		int z = 0;
+		for (int i = 0; i < 16; i ++) {
+			z |= tag[i] ^ data[off + len + i];
+		}
+		return z == 0;
+	}
+}
+
+}
diff --git a/SSLTLS/RecordDecryptGCM.cs b/SSLTLS/RecordDecryptGCM.cs
new file mode 100644
index 0000000..7dbae8c
--- /dev/null
+++ b/SSLTLS/RecordDecryptGCM.cs
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Text;
+
+using Crypto;
+
+namespace SSLTLS {
+
+internal class RecordDecryptGCM : RecordDecrypt {
+
+	IBlockCipher bc;
+	byte[] iv;
+	byte[] h;
+	ulong seq;
+	byte[] tmp, tag;
+
+	internal RecordDecryptGCM(IBlockCipher bc, byte[] iv)
+	{
+		this.bc = bc;
+		this.iv = new byte[12];
+		Array.Copy(iv, 0, this.iv, 0, 4);
+		h = new byte[16];
+		bc.BlockEncrypt(h);
+		seq = 0;
+		tag = new byte[16];
+		tmp = new byte[29];
+	}
+
+	internal override bool CheckLength(int len)
+	{
+		return len >= 24 && len <= (16384 + 24);
+	}
+
+	internal override bool Decrypt(int recordType, int version,
+		byte[] data, ref int off, ref int len)
+	{
+		off += 8;
+		len -= 24;
+		IO.Enc64be(seq, tmp, 0);
+		IO.WriteHeader(recordType, version, len, tmp, 8);
+		IO.Enc64be(13 << 3, tmp, 13);
+		IO.Enc64be((ulong)len << 3, tmp, 21);
+		for (int i = 0; i < 16; i ++) {
+			tag[i] = 0;
+		}
+		GHASH.Run(tag, h, tmp, 0, 13);
+		GHASH.Run(tag, h, data, off, len);
+		GHASH.Run(tag, h, tmp, 13, 16);
+		seq ++;
+
+		Array.Copy(data, off - 8, iv, 4, 8);
+		bc.CTRRun(iv, 2, data, off, len);
+		bc.CTRRun(iv, 1, data, off + len, 16);
+
+		int z = 0;
+		for (int i = 0; i < 16; i ++) {
+			z |= tag[i] ^ data[off + len + i];
+		}
+		return z == 0;
+	}
+}
+
+}
diff --git a/SSLTLS/RecordDecryptPlain.cs b/SSLTLS/RecordDecryptPlain.cs
new file mode 100644
index 0000000..c20b6e8
--- /dev/null
+++ b/SSLTLS/RecordDecryptPlain.cs
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Text;
+
+using Crypto;
+
+namespace SSLTLS {
+
+internal class RecordDecryptPlain : RecordDecrypt {
+
+	internal override bool CheckLength(int len)
+	{
+		return len <= 16384;
+	}
+
+	internal override bool Decrypt(int recordType, int version,
+		byte[] data, ref int off, ref int len)
+	{
+		return true;
+	}
+}
+
+}
diff --git a/SSLTLS/RecordEncrypt.cs b/SSLTLS/RecordEncrypt.cs
new file mode 100644
index 0000000..af6c361
--- /dev/null
+++ b/SSLTLS/RecordEncrypt.cs
@@ -0,0 +1,49 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Text;
+
+using Crypto;
+
+namespace SSLTLS {
+
+internal abstract class RecordEncrypt {
+
+	/*
+	 * Adjust index for plaintext. This function must reserve room
+	 * for header, MAC, padding...
+	 */
+	internal abstract void GetMaxPlaintext(ref int start, ref int end);
+
+	/*
+	 * Perform encryption. The plaintext offset and length are provided;
+	 * upon exit, they are modified to designate the complete record
+	 * (including the header).
+	 */
+	internal abstract void Encrypt(int recordType, int version,
+		byte[] data, ref int off, ref int len);
+}
+
+}
diff --git a/SSLTLS/RecordEncryptCBC.cs b/SSLTLS/RecordEncryptCBC.cs
new file mode 100644
index 0000000..b0c55fe
--- /dev/null
+++ b/SSLTLS/RecordEncryptCBC.cs
@@ -0,0 +1,180 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Text;
+
+using Crypto;
+
+namespace SSLTLS {
+
+internal class RecordEncryptCBC : RecordEncrypt {
+
+	IBlockCipher bc;
+	HMAC hm;
+	byte[] iv;
+	bool explicitIV;
+	ulong seq;
+	byte[] tmp;
+
+	internal RecordEncryptCBC(IBlockCipher bc, HMAC hm, byte[] iv)
+	{
+		this.bc = bc;
+		this.hm = hm;
+		this.iv = new byte[bc.BlockSize];
+		if (iv == null) {
+			explicitIV = true;
+		} else {
+			Array.Copy(iv, 0, this.iv, 0, iv.Length);
+			explicitIV = false;
+		}
+		seq = 0;
+		tmp = new byte[Math.Max(13, hm.MACSize)];
+	}
+
+	internal override void GetMaxPlaintext(ref int start, ref int end)
+	{
+		/*
+		 * Add room for the record header.
+		 */
+		start += 5;
+
+		int blen = bc.BlockSize;
+		if (explicitIV) {
+			start += blen;
+		} else {
+			/*
+			 * We reserve room for an automatic 1/n-1 split.
+			 */
+			start += 4 + ((hm.MACSize + blen + 1) & ~(blen - 1));
+		}
+		int len = (end - start) & ~(blen - 1);
+		len -= 1 + hm.MACSize;
+
+		/*
+		 * We keep a bit of extra room to try out overlong padding.
+		 */
+		len -= blen;
+
+		if (len > 16384) {
+			len = 16384;
+		}
+		end = start + len;
+	}
+
+	internal override void Encrypt(int recordType, int version,
+		byte[] data, ref int off, ref int len)
+	{
+		if (explicitIV
+			|| recordType != SSL.APPLICATION_DATA
+			|| len <= 1)
+		{
+			EncryptInner(recordType, version,
+				data, ref off, ref len);
+			return;
+		}
+
+		/*
+		 * Automatic 1/n-1 split. We do it only when there is
+		 * no explicit IV (i.e. TLS 1.0, not TLS 1.1+), and there
+		 * are at least two plaintext bytes in the record.
+		 */
+		int blen = bc.BlockSize;
+		int off1 = off - (4 + ((hm.MACSize + blen + 1) & ~(blen - 1)));
+		int len1 = 1;
+		data[off1] = data[off];
+		EncryptInner(recordType, version, data, ref off1, ref len1);
+		int off2 = off + 1;
+		int len2 = len - 1;
+		EncryptInner(recordType, version, data, ref off2, ref len2);
+		if (off1 + len1 != off2) {
+			throw new Exception("Split gone wrong");
+		}
+		off = off1;
+		len = len1 + len2;
+	}
+
+	void EncryptInner(int recordType, int version,
+		byte[] data, ref int off, ref int len)
+	{
+		int blen = bc.BlockSize;
+		int mlen = hm.MACSize;
+		int doff = off;
+		int dlen = len;
+
+		if (explicitIV) {
+			/*
+			 * To make pseudorandom IV, we reuse HMAC, computed
+			 * over the encoded sequence number. Since this
+			 * input is distinct from all other HMAC inputs with
+			 * the same key, this should be randomish enough
+			 * (assuming HMAC is a good imitation of a random
+			 * oracle).
+			 */
+			IO.Enc64be(seq, tmp, 0);
+			hm.Update(tmp, 0, 8);
+			hm.DoFinal(tmp, 0);
+			Array.Copy(tmp, 0, data, off - blen, blen);
+			off -= blen;
+			len += blen;
+		}
+
+		/*
+		 * Compute HMAC.
+		 */
+		IO.Enc64be(seq, tmp, 0);
+		IO.WriteHeader(recordType, version, dlen, tmp, 8);
+		hm.Update(tmp, 0, 13);
+		hm.Update(data, doff, dlen);
+		hm.DoFinal(data, off + len);
+		len += mlen;
+		seq ++;
+
+		/*
+		 * Add padding.
+		 */
+		int plen = blen - (len & (blen - 1));
+		for (int i = 0; i < plen; i ++) {
+			data[off + len + i] = (byte)(plen - 1);
+		}
+		len += plen;
+
+		/*
+		 * Perform CBC encryption. We use our saved IV. If there is
+		 * an explicit IV, then it gets encrypted, which is fine
+		 * (CBC encryption of randomness is equally good randomness).
+		 */
+		bc.CBCEncrypt(iv, data, off, len);
+		Array.Copy(data, off + len - blen, iv, 0, blen);
+
+		/*
+		 * Add the header.
+		 */
+		off -= 5;
+		IO.WriteHeader(recordType, version, len, data, off);
+		len += 5;
+	}
+}
+
+}
diff --git a/SSLTLS/RecordEncryptChaPol.cs b/SSLTLS/RecordEncryptChaPol.cs
new file mode 100644
index 0000000..ead61dd
--- /dev/null
+++ b/SSLTLS/RecordEncryptChaPol.cs
@@ -0,0 +1,106 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Text;
+
+using Crypto;
+
+namespace SSLTLS {
+
+internal class RecordEncryptChaPol : RecordEncrypt {
+
+	Poly1305 pp;
+	byte[] iv;
+	byte[] nonce;
+	byte[] tmp;
+	byte[] tag;
+	ulong seq;
+
+	internal RecordEncryptChaPol(Poly1305 pp, byte[] iv)
+	{
+		this.pp = pp;
+		this.iv = new byte[12];
+		Array.Copy(iv, 0, this.iv, 0, 12);
+		nonce = new byte[12];
+		tmp = new byte[13];
+		tag = new byte[16];
+		seq = 0;
+	}
+
+	internal override void GetMaxPlaintext(ref int start, ref int end)
+	{
+		/*
+		 * We need room at the start for the record header (5 bytes)
+		 * and some at the end for the MAC (16 bytes).
+		 */
+		start += 5;
+		end -= 16;
+		int len = Math.Min(end - start, 16384);
+		end = start + len;
+	}
+
+	internal override void Encrypt(int recordType, int version,
+		byte[] data, ref int off, ref int len)
+	{
+		/*
+		 * Make the "additional data" for the MAC:
+		 *  -- sequence number (8 bytes, big-endian)
+		 *  -- header with plaintext length (5 bytes)
+		 */
+		IO.Enc64be(seq, tmp, 0);
+		IO.WriteHeader(recordType, version, len, tmp, 8);
+
+		/*
+		 * The ChaCha20+Poly1305 IV consists in the
+		 * implicit IV (12 bytes), with the sequence number
+		 * "XORed" in the last 8 bytes (big-endian).
+		 */
+		Array.Copy(iv, 0, nonce, 0, 12);
+		for (int i = 0; i < 8; i ++) {
+			nonce[i + 4] ^= tmp[i];
+		}
+
+		/*
+		 * Do encryption and compute tag.
+		 */
+		pp.Run(nonce, data, off, len, tmp, 0, 13, tag, true);
+
+		/*
+		 * Copy back tag where appropriate and add header.
+		 */
+		Array.Copy(tag, 0, data, off + len, 16);
+		off -= 5;
+		len += 16;
+		IO.WriteHeader(recordType, version, len, data, off);
+		len += 5;
+
+		/*
+		 * Each record has its own sequence number.
+		 */
+		seq ++;
+	}
+}
+
+}
diff --git a/SSLTLS/RecordEncryptGCM.cs b/SSLTLS/RecordEncryptGCM.cs
new file mode 100644
index 0000000..2255155
--- /dev/null
+++ b/SSLTLS/RecordEncryptGCM.cs
@@ -0,0 +1,118 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Text;
+
+using Crypto;
+
+namespace SSLTLS {
+
+internal class RecordEncryptGCM : RecordEncrypt {
+
+	IBlockCipher bc;
+	byte[] iv;
+	byte[] h;
+	ulong seq;
+	byte[] tmp, tag;
+
+	internal RecordEncryptGCM(IBlockCipher bc, byte[] iv)
+	{
+		this.bc = bc;
+		this.iv = new byte[12];
+		Array.Copy(iv, 0, this.iv, 0, 4);
+		h = new byte[16];
+		bc.BlockEncrypt(h);
+		seq = 0;
+		tag = new byte[16];
+		tmp = new byte[29];
+	}
+
+	internal override void GetMaxPlaintext(ref int start, ref int end)
+	{
+		/*
+		 * We need room at the start for the record header (5 bytes)
+		 * and the explicit nonce (8 bytes). We need room at the end
+		 * for the MAC (16 bytes).
+		 */
+		start += 13;
+		end -= 16;
+		int len = Math.Min(end - start, 16384);
+		end = start + len;
+	}
+
+	internal override void Encrypt(int recordType, int version,
+		byte[] data, ref int off, ref int len)
+	{
+		/*
+		 * Explicit nonce is the encoded sequence number. We
+		 * encrypt the data itself; the counter starts at 2
+		 * (value 1 is for the authentication tag).
+		 */
+		IO.Enc64be(seq, data, off - 8);
+		Array.Copy(data, off - 8, iv, 4, 8);
+		bc.CTRRun(iv, 2, data, off, len);
+
+		/*
+		 * For the authentication tag:
+		 *   header = sequence + 5-byte "plain" header
+		 *   footer = the two relevant lengths (in bits)
+		 */
+		IO.Enc64be(seq, tmp, 0);
+		IO.WriteHeader(recordType, version, len, tmp, 8);
+		IO.Enc64be(13 << 3, tmp, 13);
+		IO.Enc64be((ulong)len << 3, tmp, 21);
+
+		/*
+		 * Compute clear authentication tag.
+		 */
+		for (int i = 0; i < 16; i ++) {
+			tag[i] = 0;
+		}
+		GHASH.Run(tag, h, tmp, 0, 13);
+		GHASH.Run(tag, h, data, off, len);
+		GHASH.Run(tag, h, tmp, 13, 16);
+
+		/*
+		 * Copy authentication tag and apply final encryption on it.
+		 */
+		Array.Copy(tag, 0, data, off + len, 16);
+		bc.CTRRun(iv, 1, data, off + len, 16);
+
+		/*
+		 * Each record uses one sequence number.
+		 */
+		seq ++;
+
+		/*
+		 * Write encrypted header and return adjusted offset/length.
+		 */
+		off -= 13;
+		len += 24;
+		IO.WriteHeader(recordType, version, len, data, off);
+		len += 5;
+	}
+}
+
+}
diff --git a/SSLTLS/RecordEncryptPlain.cs b/SSLTLS/RecordEncryptPlain.cs
new file mode 100644
index 0000000..3a87802
--- /dev/null
+++ b/SSLTLS/RecordEncryptPlain.cs
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Text;
+
+using Crypto;
+
+namespace SSLTLS {
+
+internal class RecordEncryptPlain : RecordEncrypt {
+
+	internal RecordEncryptPlain()
+	{
+	}
+
+	internal override void GetMaxPlaintext(ref int start, ref int end)
+	{
+		start += 5;
+		end = Math.Min(end, start + 16384);
+	}
+
+	internal override void Encrypt(int recordType, int version,
+		byte[] data, ref int off, ref int len)
+	{
+		off -= 5;
+		IO.WriteHeader(recordType, version, len, data, off);
+		len += 5;
+	}
+}
+
+}
diff --git a/SSLTLS/SSL.cs b/SSLTLS/SSL.cs
new file mode 100644
index 0000000..5250e5d
--- /dev/null
+++ b/SSLTLS/SSL.cs
@@ -0,0 +1,859 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Security.Cryptography.X509Certificates;
+using System.Text;
+
+using Asn1;
+using Crypto;
+using XKeys;
+
+namespace SSLTLS {
+
+/*
+ * A fake class that serves as container for various constants.
+ */
+
+public sealed class SSL {
+
+	/*
+	 * Protocol versions.
+	 */
+	public const int SSL30 = 0x0300;
+	public const int TLS10 = 0x0301;
+	public const int TLS11 = 0x0302;
+	public const int TLS12 = 0x0303;
+
+	/*
+	 * Record types.
+	 */
+	public const int CHANGE_CIPHER_SPEC  = 20;
+	public const int ALERT               = 21;
+	public const int HANDSHAKE           = 22;
+	public const int APPLICATION_DATA    = 23;
+
+	/*
+	 * Alert levels.
+	 */
+	public const int WARNING  = 1;
+	public const int FATAL    = 2;
+
+	/*
+	 * Alert messages.
+	 */
+	public const int CLOSE_NOTIFY             = 0;
+	public const int UNEXPECTED_MESSAGE       = 10;
+	public const int BAD_RECORD_MAC           = 20;
+	public const int DECRYPTION_FAILED        = 21;
+	public const int RECORD_OVERFLOW          = 22;
+	public const int DECOMPRESSION_FAILURE    = 30;
+	public const int HANDSHAKE_FAILURE        = 40;
+	public const int BAD_CERTIFICATE          = 42;
+	public const int UNSUPPORTED_CERTIFICATE  = 43;
+	public const int CERTIFICATE_REVOKED      = 44;
+	public const int CERTIFICATE_EXPIRED      = 45;
+	public const int CERTIFICATE_UNKNOWN      = 46;
+	public const int ILLEGAL_PARAMETER        = 47;
+	public const int UNKNOWN_CA               = 48;
+	public const int ACCESS_DENIED            = 49;
+	public const int DECODE_ERROR             = 50;
+	public const int DECRYPT_ERROR            = 51;
+	public const int EXPORT_RESTRICTION       = 60;
+	public const int PROTOCOL_VERSION         = 70;
+	public const int INSUFFICIENT_SECURITY    = 71;
+	public const int INTERNAL_ERROR           = 80;
+	public const int USER_CANCELED            = 90;
+	public const int NO_RENEGOTIATION         = 100;
+
+	/*
+	 * Handshake message types.
+	 */
+	public const int HELLO_REQUEST        = 0;
+	public const int CLIENT_HELLO         = 1;
+	public const int SERVER_HELLO         = 2;
+	public const int CERTIFICATE          = 11;
+	public const int SERVER_KEY_EXCHANGE  = 12;
+	public const int CERTIFICATE_REQUEST  = 13;
+	public const int SERVER_HELLO_DONE    = 14;
+	public const int CERTIFICATE_VERIFY   = 15;
+	public const int CLIENT_KEY_EXCHANGE  = 16;
+	public const int FINISHED             = 20;
+
+	/*
+	 * Cipher suites.
+	 */
+
+	/* From RFC 5246 */
+	public const int NULL_WITH_NULL_NULL                    = 0x0000;
+	public const int RSA_WITH_NULL_MD5                      = 0x0001;
+	public const int RSA_WITH_NULL_SHA                      = 0x0002;
+	public const int RSA_WITH_NULL_SHA256                   = 0x003B;
+	public const int RSA_WITH_RC4_128_MD5                   = 0x0004;
+	public const int RSA_WITH_RC4_128_SHA                   = 0x0005;
+	public const int RSA_WITH_3DES_EDE_CBC_SHA              = 0x000A;
+	public const int RSA_WITH_AES_128_CBC_SHA               = 0x002F;
+	public const int RSA_WITH_AES_256_CBC_SHA               = 0x0035;
+	public const int RSA_WITH_AES_128_CBC_SHA256            = 0x003C;
+	public const int RSA_WITH_AES_256_CBC_SHA256            = 0x003D;
+	public const int DH_DSS_WITH_3DES_EDE_CBC_SHA           = 0x000D;
+	public const int DH_RSA_WITH_3DES_EDE_CBC_SHA           = 0x0010;
+	public const int DHE_DSS_WITH_3DES_EDE_CBC_SHA          = 0x0013;
+	public const int DHE_RSA_WITH_3DES_EDE_CBC_SHA          = 0x0016;
+	public const int DH_DSS_WITH_AES_128_CBC_SHA            = 0x0030;
+	public const int DH_RSA_WITH_AES_128_CBC_SHA            = 0x0031;
+	public const int DHE_DSS_WITH_AES_128_CBC_SHA           = 0x0032;
+	public const int DHE_RSA_WITH_AES_128_CBC_SHA           = 0x0033;
+	public const int DH_DSS_WITH_AES_256_CBC_SHA            = 0x0036;
+	public const int DH_RSA_WITH_AES_256_CBC_SHA            = 0x0037;
+	public const int DHE_DSS_WITH_AES_256_CBC_SHA           = 0x0038;
+	public const int DHE_RSA_WITH_AES_256_CBC_SHA           = 0x0039;
+	public const int DH_DSS_WITH_AES_128_CBC_SHA256         = 0x003E;
+	public const int DH_RSA_WITH_AES_128_CBC_SHA256         = 0x003F;
+	public const int DHE_DSS_WITH_AES_128_CBC_SHA256        = 0x0040;
+	public const int DHE_RSA_WITH_AES_128_CBC_SHA256        = 0x0067;
+	public const int DH_DSS_WITH_AES_256_CBC_SHA256         = 0x0068;
+	public const int DH_RSA_WITH_AES_256_CBC_SHA256         = 0x0069;
+	public const int DHE_DSS_WITH_AES_256_CBC_SHA256        = 0x006A;
+	public const int DHE_RSA_WITH_AES_256_CBC_SHA256        = 0x006B;
+	public const int DH_anon_WITH_RC4_128_MD5               = 0x0018;
+	public const int DH_anon_WITH_3DES_EDE_CBC_SHA          = 0x001B;
+	public const int DH_anon_WITH_AES_128_CBC_SHA           = 0x0034;
+	public const int DH_anon_WITH_AES_256_CBC_SHA           = 0x003A;
+	public const int DH_anon_WITH_AES_128_CBC_SHA256        = 0x006C;
+	public const int DH_anon_WITH_AES_256_CBC_SHA256        = 0x006D;
+
+	/* From RFC 4492 */
+	public const int ECDH_ECDSA_WITH_NULL_SHA               = 0xC001;
+	public const int ECDH_ECDSA_WITH_RC4_128_SHA            = 0xC002;
+	public const int ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA       = 0xC003;
+	public const int ECDH_ECDSA_WITH_AES_128_CBC_SHA        = 0xC004;
+	public const int ECDH_ECDSA_WITH_AES_256_CBC_SHA        = 0xC005;
+	public const int ECDHE_ECDSA_WITH_NULL_SHA              = 0xC006;
+	public const int ECDHE_ECDSA_WITH_RC4_128_SHA           = 0xC007;
+	public const int ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA      = 0xC008;
+	public const int ECDHE_ECDSA_WITH_AES_128_CBC_SHA       = 0xC009;
+	public const int ECDHE_ECDSA_WITH_AES_256_CBC_SHA       = 0xC00A;
+	public const int ECDH_RSA_WITH_NULL_SHA                 = 0xC00B;
+	public const int ECDH_RSA_WITH_RC4_128_SHA              = 0xC00C;
+	public const int ECDH_RSA_WITH_3DES_EDE_CBC_SHA         = 0xC00D;
+	public const int ECDH_RSA_WITH_AES_128_CBC_SHA          = 0xC00E;
+	public const int ECDH_RSA_WITH_AES_256_CBC_SHA          = 0xC00F;
+	public const int ECDHE_RSA_WITH_NULL_SHA                = 0xC010;
+	public const int ECDHE_RSA_WITH_RC4_128_SHA             = 0xC011;
+	public const int ECDHE_RSA_WITH_3DES_EDE_CBC_SHA        = 0xC012;
+	public const int ECDHE_RSA_WITH_AES_128_CBC_SHA         = 0xC013;
+	public const int ECDHE_RSA_WITH_AES_256_CBC_SHA         = 0xC014;
+	public const int ECDH_anon_WITH_NULL_SHA                = 0xC015;
+	public const int ECDH_anon_WITH_RC4_128_SHA             = 0xC016;
+	public const int ECDH_anon_WITH_3DES_EDE_CBC_SHA        = 0xC017;
+	public const int ECDH_anon_WITH_AES_128_CBC_SHA         = 0xC018;
+	public const int ECDH_anon_WITH_AES_256_CBC_SHA         = 0xC019;
+
+	/* From RFC 5288 */
+	public const int RSA_WITH_AES_128_GCM_SHA256            = 0x009C;
+	public const int RSA_WITH_AES_256_GCM_SHA384            = 0x009D;
+	public const int DHE_RSA_WITH_AES_128_GCM_SHA256        = 0x009E;
+	public const int DHE_RSA_WITH_AES_256_GCM_SHA384        = 0x009F;
+	public const int DH_RSA_WITH_AES_128_GCM_SHA256         = 0x00A0;
+	public const int DH_RSA_WITH_AES_256_GCM_SHA384         = 0x00A1;
+	public const int DHE_DSS_WITH_AES_128_GCM_SHA256        = 0x00A2;
+	public const int DHE_DSS_WITH_AES_256_GCM_SHA384        = 0x00A3;
+	public const int DH_DSS_WITH_AES_128_GCM_SHA256         = 0x00A4;
+	public const int DH_DSS_WITH_AES_256_GCM_SHA384         = 0x00A5;
+	public const int DH_anon_WITH_AES_128_GCM_SHA256        = 0x00A6;
+	public const int DH_anon_WITH_AES_256_GCM_SHA384        = 0x00A7;
+
+	/* From RFC 5289 */
+	public const int ECDHE_ECDSA_WITH_AES_128_CBC_SHA256    = 0xC023;
+	public const int ECDHE_ECDSA_WITH_AES_256_CBC_SHA384    = 0xC024;
+	public const int ECDH_ECDSA_WITH_AES_128_CBC_SHA256     = 0xC025;
+	public const int ECDH_ECDSA_WITH_AES_256_CBC_SHA384     = 0xC026;
+	public const int ECDHE_RSA_WITH_AES_128_CBC_SHA256      = 0xC027;
+	public const int ECDHE_RSA_WITH_AES_256_CBC_SHA384      = 0xC028;
+	public const int ECDH_RSA_WITH_AES_128_CBC_SHA256       = 0xC029;
+	public const int ECDH_RSA_WITH_AES_256_CBC_SHA384       = 0xC02A;
+	public const int ECDHE_ECDSA_WITH_AES_128_GCM_SHA256    = 0xC02B;
+	public const int ECDHE_ECDSA_WITH_AES_256_GCM_SHA384    = 0xC02C;
+	public const int ECDH_ECDSA_WITH_AES_128_GCM_SHA256     = 0xC02D;
+	public const int ECDH_ECDSA_WITH_AES_256_GCM_SHA384     = 0xC02E;
+	public const int ECDHE_RSA_WITH_AES_128_GCM_SHA256      = 0xC02F;
+	public const int ECDHE_RSA_WITH_AES_256_GCM_SHA384      = 0xC030;
+	public const int ECDH_RSA_WITH_AES_128_GCM_SHA256       = 0xC031;
+	public const int ECDH_RSA_WITH_AES_256_GCM_SHA384       = 0xC032;
+
+	/* From RFC 7905 */
+	public const int ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256      = 0xCCA8;
+	public const int ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256    = 0xCCA9;
+	public const int DHE_RSA_WITH_CHACHA20_POLY1305_SHA256        = 0xCCAA;
+	public const int PSK_WITH_CHACHA20_POLY1305_SHA256            = 0xCCAB;
+	public const int ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256      = 0xCCAC;
+	public const int DHE_PSK_WITH_CHACHA20_POLY1305_SHA256        = 0xCCAD;
+	public const int RSA_PSK_WITH_CHACHA20_POLY1305_SHA256        = 0xCCAE;
+
+	/* From RFC 7507 */
+	public const int FALLBACK_SCSV                          = 0x5600;
+
+	/* From RFC 5746 */
+	public const int EMPTY_RENEGOTIATION_INFO_SCSV          = 0x00FF;
+
+	/*
+	 * Client certificate types.
+	 */
+	public const int RSA_SIGN      = 1;
+	public const int DSS_SIGN      = 2;
+	public const int RSA_FIXED_DH  = 3;
+	public const int DSS_FIXED_DH  = 4;
+
+	/*
+	 * Hash algorithm identifiers. The special "MD5SHA1" is for use
+	 * with RSA signatures in TLS 1.0 and 1.1 only.
+	 */
+	public const int MD5SHA1 = 0;
+	public const int MD5     = 1;
+	public const int SHA1    = 2;
+	public const int SHA224  = 3;
+	public const int SHA256  = 4;
+	public const int SHA384  = 5;
+	public const int SHA512  = 6;
+
+	/*
+	 * Signature algorithm identifiers.
+	 */
+	public const int RSA     = 1;
+	public const int DSA     = 2;
+	public const int ECDSA   = 3;
+
+	/*
+	 * Combined hash-and-sign algorithms.
+	 */
+	public const int RSA_MD5SHA1   = (MD5SHA1 << 8) + RSA;
+	public const int RSA_MD5       = (MD5 << 8) + RSA;
+	public const int RSA_SHA1      = (SHA1 << 8) + RSA;
+	public const int RSA_SHA224    = (SHA224 << 8) + RSA;
+	public const int RSA_SHA256    = (SHA256 << 8) + RSA;
+	public const int RSA_SHA384    = (SHA384 << 8) + RSA;
+	public const int RSA_SHA512    = (SHA512 << 8) + RSA;
+	public const int ECDSA_MD5     = (MD5 << 8) + ECDSA;
+	public const int ECDSA_SHA1    = (SHA1 << 8) + ECDSA;
+	public const int ECDSA_SHA224  = (SHA224 << 8) + ECDSA;
+	public const int ECDSA_SHA256  = (SHA256 << 8) + ECDSA;
+	public const int ECDSA_SHA384  = (SHA384 << 8) + ECDSA;
+	public const int ECDSA_SHA512  = (SHA512 << 8) + ECDSA;
+
+	/*
+	 * Symbolic identifiers for named curves.
+	 */
+	public const int NIST_P256   = 23;
+	public const int NIST_P384   = 24;
+	public const int NIST_P521   = 25;
+	public const int Curve25519  = 29;
+
+	/*
+	 * Get a human-readable name for a version.
+	 */
+	public static string VersionName(int version)
+	{
+		switch (version) {
+		case SSL30: return "SSL 3.0";
+		case TLS10: return "TLS 1.0";
+		case TLS11: return "TLS 1.1";
+		case TLS12: return "TLS 1.2";
+		}
+		if ((version >> 8) == 3) {
+			return String.Format("TLS 1.{0}", (version & 0xFF) - 1);
+		}
+		return String.Format("UNKNOWN:0x{0:X4}", version);
+	}
+
+	/*
+	 * Get a human-readable name for a cipher suite.
+	 */
+	public static string CipherSuiteName(int cipherSuite)
+	{
+		switch (cipherSuite) {
+		case NULL_WITH_NULL_NULL:
+			return "NULL_WITH_NULL_NULL";
+		case RSA_WITH_NULL_MD5:
+			return "RSA_WITH_NULL_MD5";
+		case RSA_WITH_NULL_SHA:
+			return "RSA_WITH_NULL_SHA";
+		case RSA_WITH_NULL_SHA256:
+			return "RSA_WITH_NULL_SHA256";
+		case RSA_WITH_RC4_128_MD5:
+			return "RSA_WITH_RC4_128_MD5";
+		case RSA_WITH_RC4_128_SHA:
+			return "RSA_WITH_RC4_128_SHA";
+		case RSA_WITH_3DES_EDE_CBC_SHA:
+			return "RSA_WITH_3DES_EDE_CBC_SHA";
+		case RSA_WITH_AES_128_CBC_SHA:
+			return "RSA_WITH_AES_128_CBC_SHA";
+		case RSA_WITH_AES_256_CBC_SHA:
+			return "RSA_WITH_AES_256_CBC_SHA";
+		case RSA_WITH_AES_128_CBC_SHA256:
+			return "RSA_WITH_AES_128_CBC_SHA256";
+		case RSA_WITH_AES_256_CBC_SHA256:
+			return "RSA_WITH_AES_256_CBC_SHA256";
+		case DH_DSS_WITH_3DES_EDE_CBC_SHA:
+			return "DH_DSS_WITH_3DES_EDE_CBC_SHA";
+		case DH_RSA_WITH_3DES_EDE_CBC_SHA:
+			return "DH_RSA_WITH_3DES_EDE_CBC_SHA";
+		case DHE_DSS_WITH_3DES_EDE_CBC_SHA:
+			return "DHE_DSS_WITH_3DES_EDE_CBC_SHA";
+		case DHE_RSA_WITH_3DES_EDE_CBC_SHA:
+			return "DHE_RSA_WITH_3DES_EDE_CBC_SHA";
+		case DH_DSS_WITH_AES_128_CBC_SHA:
+			return "DH_DSS_WITH_AES_128_CBC_SHA";
+		case DH_RSA_WITH_AES_128_CBC_SHA:
+			return "DH_RSA_WITH_AES_128_CBC_SHA";
+		case DHE_DSS_WITH_AES_128_CBC_SHA:
+			return "DHE_DSS_WITH_AES_128_CBC_SHA";
+		case DHE_RSA_WITH_AES_128_CBC_SHA:
+			return "DHE_RSA_WITH_AES_128_CBC_SHA";
+		case DH_DSS_WITH_AES_256_CBC_SHA:
+			return "DH_DSS_WITH_AES_256_CBC_SHA";
+		case DH_RSA_WITH_AES_256_CBC_SHA:
+			return "DH_RSA_WITH_AES_256_CBC_SHA";
+		case DHE_DSS_WITH_AES_256_CBC_SHA:
+			return "DHE_DSS_WITH_AES_256_CBC_SHA";
+		case DHE_RSA_WITH_AES_256_CBC_SHA:
+			return "DHE_RSA_WITH_AES_256_CBC_SHA";
+		case DH_DSS_WITH_AES_128_CBC_SHA256:
+			return "DH_DSS_WITH_AES_128_CBC_SHA256";
+		case DH_RSA_WITH_AES_128_CBC_SHA256:
+			return "DH_RSA_WITH_AES_128_CBC_SHA256";
+		case DHE_DSS_WITH_AES_128_CBC_SHA256:
+			return "DHE_DSS_WITH_AES_128_CBC_SHA256";
+		case DHE_RSA_WITH_AES_128_CBC_SHA256:
+			return "DHE_RSA_WITH_AES_128_CBC_SHA256";
+		case DH_DSS_WITH_AES_256_CBC_SHA256:
+			return "DH_DSS_WITH_AES_256_CBC_SHA256";
+		case DH_RSA_WITH_AES_256_CBC_SHA256:
+			return "DH_RSA_WITH_AES_256_CBC_SHA256";
+		case DHE_DSS_WITH_AES_256_CBC_SHA256:
+			return "DHE_DSS_WITH_AES_256_CBC_SHA256";
+		case DHE_RSA_WITH_AES_256_CBC_SHA256:
+			return "DHE_RSA_WITH_AES_256_CBC_SHA256";
+		case DH_anon_WITH_RC4_128_MD5:
+			return "DH_anon_WITH_RC4_128_MD5";
+		case DH_anon_WITH_3DES_EDE_CBC_SHA:
+			return "DH_anon_WITH_3DES_EDE_CBC_SHA";
+		case DH_anon_WITH_AES_128_CBC_SHA:
+			return "DH_anon_WITH_AES_128_CBC_SHA";
+		case DH_anon_WITH_AES_256_CBC_SHA:
+			return "DH_anon_WITH_AES_256_CBC_SHA";
+		case DH_anon_WITH_AES_128_CBC_SHA256:
+			return "DH_anon_WITH_AES_128_CBC_SHA256";
+		case DH_anon_WITH_AES_256_CBC_SHA256:
+			return "DH_anon_WITH_AES_256_CBC_SHA256";
+		case ECDH_ECDSA_WITH_NULL_SHA:
+			return "ECDH_ECDSA_WITH_NULL_SHA";
+		case ECDH_ECDSA_WITH_RC4_128_SHA:
+			return "ECDH_ECDSA_WITH_RC4_128_SHA";
+		case ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA:
+			return "ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA";
+		case ECDH_ECDSA_WITH_AES_128_CBC_SHA:
+			return "ECDH_ECDSA_WITH_AES_128_CBC_SHA";
+		case ECDH_ECDSA_WITH_AES_256_CBC_SHA:
+			return "ECDH_ECDSA_WITH_AES_256_CBC_SHA";
+		case ECDHE_ECDSA_WITH_NULL_SHA:
+			return "ECDHE_ECDSA_WITH_NULL_SHA";
+		case ECDHE_ECDSA_WITH_RC4_128_SHA:
+			return "ECDHE_ECDSA_WITH_RC4_128_SHA";
+		case ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA:
+			return "ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA";
+		case ECDHE_ECDSA_WITH_AES_128_CBC_SHA:
+			return "ECDHE_ECDSA_WITH_AES_128_CBC_SHA";
+		case ECDHE_ECDSA_WITH_AES_256_CBC_SHA:
+			return "ECDHE_ECDSA_WITH_AES_256_CBC_SHA";
+		case ECDH_RSA_WITH_NULL_SHA:
+			return "ECDH_RSA_WITH_NULL_SHA";
+		case ECDH_RSA_WITH_RC4_128_SHA:
+			return "ECDH_RSA_WITH_RC4_128_SHA";
+		case ECDH_RSA_WITH_3DES_EDE_CBC_SHA:
+			return "ECDH_RSA_WITH_3DES_EDE_CBC_SHA";
+		case ECDH_RSA_WITH_AES_128_CBC_SHA:
+			return "ECDH_RSA_WITH_AES_128_CBC_SHA";
+		case ECDH_RSA_WITH_AES_256_CBC_SHA:
+			return "ECDH_RSA_WITH_AES_256_CBC_SHA";
+		case ECDHE_RSA_WITH_NULL_SHA:
+			return "ECDHE_RSA_WITH_NULL_SHA";
+		case ECDHE_RSA_WITH_RC4_128_SHA:
+			return "ECDHE_RSA_WITH_RC4_128_SHA";
+		case ECDHE_RSA_WITH_3DES_EDE_CBC_SHA:
+			return "ECDHE_RSA_WITH_3DES_EDE_CBC_SHA";
+		case ECDHE_RSA_WITH_AES_128_CBC_SHA:
+			return "ECDHE_RSA_WITH_AES_128_CBC_SHA";
+		case ECDHE_RSA_WITH_AES_256_CBC_SHA:
+			return "ECDHE_RSA_WITH_AES_256_CBC_SHA";
+		case ECDH_anon_WITH_NULL_SHA:
+			return "ECDH_anon_WITH_NULL_SHA";
+		case ECDH_anon_WITH_RC4_128_SHA:
+			return "ECDH_anon_WITH_RC4_128_SHA";
+		case ECDH_anon_WITH_3DES_EDE_CBC_SHA:
+			return "ECDH_anon_WITH_3DES_EDE_CBC_SHA";
+		case ECDH_anon_WITH_AES_128_CBC_SHA:
+			return "ECDH_anon_WITH_AES_128_CBC_SHA";
+		case ECDH_anon_WITH_AES_256_CBC_SHA:
+			return "ECDH_anon_WITH_AES_256_CBC_SHA";
+		case RSA_WITH_AES_128_GCM_SHA256:
+			return "RSA_WITH_AES_128_GCM_SHA256";
+		case RSA_WITH_AES_256_GCM_SHA384:
+			return "RSA_WITH_AES_256_GCM_SHA384";
+		case DHE_RSA_WITH_AES_128_GCM_SHA256:
+			return "DHE_RSA_WITH_AES_128_GCM_SHA256";
+		case DHE_RSA_WITH_AES_256_GCM_SHA384:
+			return "DHE_RSA_WITH_AES_256_GCM_SHA384";
+		case DH_RSA_WITH_AES_128_GCM_SHA256:
+			return "DH_RSA_WITH_AES_128_GCM_SHA256";
+		case DH_RSA_WITH_AES_256_GCM_SHA384:
+			return "DH_RSA_WITH_AES_256_GCM_SHA384";
+		case DHE_DSS_WITH_AES_128_GCM_SHA256:
+			return "DHE_DSS_WITH_AES_128_GCM_SHA256";
+		case DHE_DSS_WITH_AES_256_GCM_SHA384:
+			return "DHE_DSS_WITH_AES_256_GCM_SHA384";
+		case DH_DSS_WITH_AES_128_GCM_SHA256:
+			return "DH_DSS_WITH_AES_128_GCM_SHA256";
+		case DH_DSS_WITH_AES_256_GCM_SHA384:
+			return "DH_DSS_WITH_AES_256_GCM_SHA384";
+		case DH_anon_WITH_AES_128_GCM_SHA256:
+			return "DH_anon_WITH_AES_128_GCM_SHA256";
+		case DH_anon_WITH_AES_256_GCM_SHA384:
+			return "DH_anon_WITH_AES_256_GCM_SHA384";
+		case ECDHE_ECDSA_WITH_AES_128_CBC_SHA256:
+			return "ECDHE_ECDSA_WITH_AES_128_CBC_SHA256";
+		case ECDHE_ECDSA_WITH_AES_256_CBC_SHA384:
+			return "ECDHE_ECDSA_WITH_AES_256_CBC_SHA384";
+		case ECDH_ECDSA_WITH_AES_128_CBC_SHA256:
+			return "ECDH_ECDSA_WITH_AES_128_CBC_SHA256";
+		case ECDH_ECDSA_WITH_AES_256_CBC_SHA384:
+			return "ECDH_ECDSA_WITH_AES_256_CBC_SHA384";
+		case ECDHE_RSA_WITH_AES_128_CBC_SHA256:
+			return "ECDHE_RSA_WITH_AES_128_CBC_SHA256";
+		case ECDHE_RSA_WITH_AES_256_CBC_SHA384:
+			return "ECDHE_RSA_WITH_AES_256_CBC_SHA384";
+		case ECDH_RSA_WITH_AES_128_CBC_SHA256:
+			return "ECDH_RSA_WITH_AES_128_CBC_SHA256";
+		case ECDH_RSA_WITH_AES_256_CBC_SHA384:
+			return "ECDH_RSA_WITH_AES_256_CBC_SHA384";
+		case ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
+			return "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256";
+		case ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:
+			return "ECDHE_ECDSA_WITH_AES_256_GCM_SHA384";
+		case ECDH_ECDSA_WITH_AES_128_GCM_SHA256:
+			return "ECDH_ECDSA_WITH_AES_128_GCM_SHA256";
+		case ECDH_ECDSA_WITH_AES_256_GCM_SHA384:
+			return "ECDH_ECDSA_WITH_AES_256_GCM_SHA384";
+		case ECDHE_RSA_WITH_AES_128_GCM_SHA256:
+			return "ECDHE_RSA_WITH_AES_128_GCM_SHA256";
+		case ECDHE_RSA_WITH_AES_256_GCM_SHA384:
+			return "ECDHE_RSA_WITH_AES_256_GCM_SHA384";
+		case ECDH_RSA_WITH_AES_128_GCM_SHA256:
+			return "ECDH_RSA_WITH_AES_128_GCM_SHA256";
+		case ECDH_RSA_WITH_AES_256_GCM_SHA384:
+			return "ECDH_RSA_WITH_AES_256_GCM_SHA384";
+		case ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:
+			return "ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256";
+		case ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:
+			return "ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256";
+		case DHE_RSA_WITH_CHACHA20_POLY1305_SHA256:
+			return "DHE_RSA_WITH_CHACHA20_POLY1305_SHA256";
+		case PSK_WITH_CHACHA20_POLY1305_SHA256:
+			return "PSK_WITH_CHACHA20_POLY1305_SHA256";
+		case ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256:
+			return "ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256";
+		case DHE_PSK_WITH_CHACHA20_POLY1305_SHA256:
+			return "DHE_PSK_WITH_CHACHA20_POLY1305_SHA256";
+		case RSA_PSK_WITH_CHACHA20_POLY1305_SHA256:
+			return "RSA_PSK_WITH_CHACHA20_POLY1305_SHA256";
+		case FALLBACK_SCSV:
+			return "FALLBACK_SCSV";
+		case EMPTY_RENEGOTIATION_INFO_SCSV:
+			return "EMPTY_RENEGOTIATION_INFO_SCSV";
+		default:
+			return String.Format("UNKNOWN:0x{0:X4}", cipherSuite);
+		}
+	}
+
+	/*
+	 * Get a human-readable name for a hash-and-sign algorithm.
+	 */
+	public static string HashAndSignName(int hs)
+	{
+		switch (hs) {
+		case RSA_MD5:       return "RSA_MD5";
+		case RSA_SHA1:      return "RSA_SHA1";
+		case RSA_SHA224:    return "RSA_SHA224";
+		case RSA_SHA256:    return "RSA_SHA256";
+		case RSA_SHA384:    return "RSA_SHA384";
+		case RSA_SHA512:    return "RSA_SHA512";
+		case ECDSA_MD5:     return "ECDSA_MD5";
+		case ECDSA_SHA1:    return "ECDSA_SHA1";
+		case ECDSA_SHA224:  return "ECDSA_SHA224";
+		case ECDSA_SHA256:  return "ECDSA_SHA256";
+		case ECDSA_SHA384:  return "ECDSA_SHA384";
+		case ECDSA_SHA512:  return "ECDSA_SHA512";
+		default:
+			return String.Format("UNKNOWN:0x{0:X4}", hs);
+		}
+	}
+
+	/*
+	 * Get a human-readable name for a curve.
+	 */
+	public static string CurveName(int id)
+	{
+		switch (id) {
+		case Curve25519:  return "Curve25519";
+		case NIST_P256:   return "NIST_P256";
+		case NIST_P384:   return "NIST_P384";
+		case NIST_P521:   return "NIST_P521";
+		default:
+			return String.Format("UNKNOWN:0x{0:X4}", id);
+		}
+	}
+
+	/*
+	 * Extract the public key from an encoded X.509 certificate.
+	 * This does NOT make any attempt at validating the certificate.
+	 */
+	internal static IPublicKey GetKeyFromCert(byte[] cert)
+	{
+		AsnElt ae = AsnElt.Decode(cert);
+		ae.CheckTag(AsnElt.SEQUENCE);
+		ae.CheckNumSub(3);
+		ae = ae.GetSub(0);
+		ae.CheckTag(AsnElt.SEQUENCE);
+		ae.CheckNumSubMin(6);
+		int off = 5;
+		if (ae.GetSub(0).TagValue != AsnElt.INTEGER) {
+			ae.CheckNumSubMin(7);
+			off ++;
+		}
+		return KF.DecodePublicKey(ae.GetSub(off));
+	}
+
+	internal static bool IsRSA(int cs)
+	{
+		switch (cs) {
+		case RSA_WITH_RC4_128_MD5:
+		case RSA_WITH_RC4_128_SHA:
+		case RSA_WITH_3DES_EDE_CBC_SHA:
+		case RSA_WITH_AES_128_CBC_SHA:
+		case RSA_WITH_AES_256_CBC_SHA:
+		case RSA_WITH_AES_128_CBC_SHA256:
+		case RSA_WITH_AES_256_CBC_SHA256:
+		case RSA_WITH_AES_128_GCM_SHA256:
+		case RSA_WITH_AES_256_GCM_SHA384:
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	internal static bool IsDH_DSA(int cs)
+	{
+		switch (cs) {
+		case DH_DSS_WITH_3DES_EDE_CBC_SHA:
+		case DH_DSS_WITH_AES_128_CBC_SHA:
+		case DH_DSS_WITH_AES_256_CBC_SHA:
+		case DH_DSS_WITH_AES_128_CBC_SHA256:
+		case DH_DSS_WITH_AES_256_CBC_SHA256:
+		case DH_DSS_WITH_AES_128_GCM_SHA256:
+		case DH_DSS_WITH_AES_256_GCM_SHA384:
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	internal static bool IsDH_RSA(int cs)
+	{
+		switch (cs) {
+		case DH_RSA_WITH_3DES_EDE_CBC_SHA:
+		case DH_RSA_WITH_AES_128_CBC_SHA:
+		case DH_RSA_WITH_AES_256_CBC_SHA:
+		case DH_RSA_WITH_AES_128_CBC_SHA256:
+		case DH_RSA_WITH_AES_256_CBC_SHA256:
+		case DH_RSA_WITH_AES_128_GCM_SHA256:
+		case DH_RSA_WITH_AES_256_GCM_SHA384:
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	internal static bool IsDH(int cs)
+	{
+		return IsDH_DSA(cs) || IsDH_RSA(cs);
+	}
+
+	internal static bool IsDHE_DSS(int cs)
+	{
+		switch (cs) {
+		case DHE_DSS_WITH_3DES_EDE_CBC_SHA:
+		case DHE_DSS_WITH_AES_128_CBC_SHA:
+		case DHE_DSS_WITH_AES_256_CBC_SHA:
+		case DHE_DSS_WITH_AES_128_CBC_SHA256:
+		case DHE_DSS_WITH_AES_256_CBC_SHA256:
+		case DHE_DSS_WITH_AES_128_GCM_SHA256:
+		case DHE_DSS_WITH_AES_256_GCM_SHA384:
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	internal static bool IsDHE_RSA(int cs)
+	{
+		switch (cs) {
+		case DHE_RSA_WITH_3DES_EDE_CBC_SHA:
+		case DHE_RSA_WITH_AES_128_CBC_SHA:
+		case DHE_RSA_WITH_AES_256_CBC_SHA:
+		case DHE_RSA_WITH_AES_128_CBC_SHA256:
+		case DHE_RSA_WITH_AES_256_CBC_SHA256:
+		case DHE_RSA_WITH_AES_128_GCM_SHA256:
+		case DHE_RSA_WITH_AES_256_GCM_SHA384:
+		case DHE_RSA_WITH_CHACHA20_POLY1305_SHA256:
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	internal static bool IsECDH_ECDSA(int cs)
+	{
+		switch (cs) {
+		case ECDH_ECDSA_WITH_NULL_SHA:
+		case ECDH_ECDSA_WITH_RC4_128_SHA:
+		case ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA:
+		case ECDH_ECDSA_WITH_AES_128_CBC_SHA:
+		case ECDH_ECDSA_WITH_AES_256_CBC_SHA:
+		case ECDH_ECDSA_WITH_AES_128_CBC_SHA256:
+		case ECDH_ECDSA_WITH_AES_256_CBC_SHA384:
+		case ECDH_ECDSA_WITH_AES_128_GCM_SHA256:
+		case ECDH_ECDSA_WITH_AES_256_GCM_SHA384:
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	internal static bool IsECDH_RSA(int cs)
+	{
+		switch (cs) {
+		case ECDH_RSA_WITH_NULL_SHA:
+		case ECDH_RSA_WITH_RC4_128_SHA:
+		case ECDH_RSA_WITH_3DES_EDE_CBC_SHA:
+		case ECDH_RSA_WITH_AES_128_CBC_SHA:
+		case ECDH_RSA_WITH_AES_256_CBC_SHA:
+		case ECDH_RSA_WITH_AES_128_CBC_SHA256:
+		case ECDH_RSA_WITH_AES_256_CBC_SHA384:
+		case ECDH_RSA_WITH_AES_128_GCM_SHA256:
+		case ECDH_RSA_WITH_AES_256_GCM_SHA384:
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	internal static bool IsECDH(int cs)
+	{
+		return IsECDH_ECDSA(cs) || IsECDH_RSA(cs);
+	}
+
+	internal static bool IsECDHE_ECDSA(int cs)
+	{
+		switch (cs) {
+		case ECDHE_ECDSA_WITH_NULL_SHA:
+		case ECDHE_ECDSA_WITH_RC4_128_SHA:
+		case ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA:
+		case ECDHE_ECDSA_WITH_AES_128_CBC_SHA:
+		case ECDHE_ECDSA_WITH_AES_256_CBC_SHA:
+		case ECDHE_ECDSA_WITH_AES_128_CBC_SHA256:
+		case ECDHE_ECDSA_WITH_AES_256_CBC_SHA384:
+		case ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
+		case ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:
+		case ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	internal static bool IsECDHE_RSA(int cs)
+	{
+		switch (cs) {
+		case ECDHE_RSA_WITH_NULL_SHA:
+		case ECDHE_RSA_WITH_RC4_128_SHA:
+		case ECDHE_RSA_WITH_3DES_EDE_CBC_SHA:
+		case ECDHE_RSA_WITH_AES_128_CBC_SHA:
+		case ECDHE_RSA_WITH_AES_256_CBC_SHA:
+		case ECDHE_RSA_WITH_AES_128_CBC_SHA256:
+		case ECDHE_RSA_WITH_AES_256_CBC_SHA384:
+		case ECDHE_RSA_WITH_AES_128_GCM_SHA256:
+		case ECDHE_RSA_WITH_AES_256_GCM_SHA384:
+		case ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	internal static bool IsECDHE(int cs)
+	{
+		return IsECDHE_RSA(cs) || IsECDHE_ECDSA(cs);
+	}
+
+	internal static bool IsSHA384(int cs)
+	{
+		switch (cs) {
+		case RSA_WITH_AES_256_GCM_SHA384:
+		case DH_DSS_WITH_AES_256_GCM_SHA384:
+		case DH_RSA_WITH_AES_256_GCM_SHA384:
+		case DHE_DSS_WITH_AES_256_GCM_SHA384:
+		case DHE_RSA_WITH_AES_256_GCM_SHA384:
+		case ECDH_ECDSA_WITH_AES_256_CBC_SHA384:
+		case ECDH_ECDSA_WITH_AES_256_GCM_SHA384:
+		case ECDH_RSA_WITH_AES_256_CBC_SHA384:
+		case ECDH_RSA_WITH_AES_256_GCM_SHA384:
+		case ECDHE_ECDSA_WITH_AES_256_CBC_SHA384:
+		case ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:
+		case ECDHE_RSA_WITH_AES_256_CBC_SHA384:
+		case ECDHE_RSA_WITH_AES_256_GCM_SHA384:
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	internal static bool IsTLS12(int cs)
+	{
+		switch (cs) {
+		case RSA_WITH_NULL_SHA256:
+		case RSA_WITH_AES_128_CBC_SHA256:
+		case RSA_WITH_AES_256_CBC_SHA256:
+		case DH_DSS_WITH_AES_128_CBC_SHA256:
+		case DH_RSA_WITH_AES_128_CBC_SHA256:
+		case DHE_DSS_WITH_AES_128_CBC_SHA256:
+		case DHE_RSA_WITH_AES_128_CBC_SHA256:
+		case DH_DSS_WITH_AES_256_CBC_SHA256:
+		case DH_RSA_WITH_AES_256_CBC_SHA256:
+		case DHE_DSS_WITH_AES_256_CBC_SHA256:
+		case DHE_RSA_WITH_AES_256_CBC_SHA256:
+		case DH_anon_WITH_AES_128_CBC_SHA256:
+		case DH_anon_WITH_AES_256_CBC_SHA256:
+		case RSA_WITH_AES_128_GCM_SHA256:
+		case RSA_WITH_AES_256_GCM_SHA384:
+		case DHE_RSA_WITH_AES_128_GCM_SHA256:
+		case DHE_RSA_WITH_AES_256_GCM_SHA384:
+		case DH_RSA_WITH_AES_128_GCM_SHA256:
+		case DH_RSA_WITH_AES_256_GCM_SHA384:
+		case DHE_DSS_WITH_AES_128_GCM_SHA256:
+		case DHE_DSS_WITH_AES_256_GCM_SHA384:
+		case DH_DSS_WITH_AES_128_GCM_SHA256:
+		case DH_DSS_WITH_AES_256_GCM_SHA384:
+		case DH_anon_WITH_AES_128_GCM_SHA256:
+		case DH_anon_WITH_AES_256_GCM_SHA384:
+		case ECDHE_ECDSA_WITH_AES_128_CBC_SHA256:
+		case ECDHE_ECDSA_WITH_AES_256_CBC_SHA384:
+		case ECDH_ECDSA_WITH_AES_128_CBC_SHA256:
+		case ECDH_ECDSA_WITH_AES_256_CBC_SHA384:
+		case ECDHE_RSA_WITH_AES_128_CBC_SHA256:
+		case ECDHE_RSA_WITH_AES_256_CBC_SHA384:
+		case ECDH_RSA_WITH_AES_128_CBC_SHA256:
+		case ECDH_RSA_WITH_AES_256_CBC_SHA384:
+		case ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
+		case ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:
+		case ECDH_ECDSA_WITH_AES_128_GCM_SHA256:
+		case ECDH_ECDSA_WITH_AES_256_GCM_SHA384:
+		case ECDHE_RSA_WITH_AES_128_GCM_SHA256:
+		case ECDHE_RSA_WITH_AES_256_GCM_SHA384:
+		case ECDH_RSA_WITH_AES_128_GCM_SHA256:
+		case ECDH_RSA_WITH_AES_256_GCM_SHA384:
+		case ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:
+		case ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:
+		case DHE_RSA_WITH_CHACHA20_POLY1305_SHA256:
+		case PSK_WITH_CHACHA20_POLY1305_SHA256:
+		case ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256:
+		case DHE_PSK_WITH_CHACHA20_POLY1305_SHA256:
+		case RSA_PSK_WITH_CHACHA20_POLY1305_SHA256:
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	internal static PRF GetPRFForTLS12(int cs)
+	{
+		return new PRF(IsSHA384(cs)
+			? (IDigest)new SHA384()
+			: (IDigest)new SHA256());
+	}
+
+	internal static ECCurve GetCurveByID(int id)
+	{
+		switch (id) {
+		case NIST_P256: return EC.P256;
+		case NIST_P384: return EC.P384;
+		case NIST_P521: return EC.P521;
+		case Curve25519: return EC.Curve25519;
+		default:
+			throw new SSLException("Unknown curve: " + id);
+		}
+	}
+
+	/*
+	 * Get ID for a curve. This returns -1 if the curve is not
+	 * recognised.
+	 */
+	internal static int CurveToID(ECCurve curve)
+	{
+		switch (curve.Name) {
+		case "P-256":       return SSL.NIST_P256;
+		case "P-384":       return SSL.NIST_P384;
+		case "P-521":       return SSL.NIST_P521;
+		case "Curve25519":  return SSL.Curve25519;
+		default:
+			return -1;
+		}
+	}
+
+	internal static IDigest GetHashByID(int id)
+	{
+		switch (id) {
+		case 1: return new MD5();
+		case 2: return new SHA1();
+		case 3: return new SHA224();
+		case 4: return new SHA256();
+		case 5: return new SHA384();
+		case 6: return new SHA512();
+		default:
+			throw new SSLException("Unknown hash: " + id);
+		}
+	}
+}
+
+}
diff --git a/SSLTLS/SSLClient.cs b/SSLTLS/SSLClient.cs
new file mode 100644
index 0000000..2122b94
--- /dev/null
+++ b/SSLTLS/SSLClient.cs
@@ -0,0 +1,905 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+using Crypto;
+
+namespace SSLTLS {
+
+/*
+ * Class for a SSL client connection.
+ *
+ * An instance is created over a specified transport stream. SSL session
+ * parameters from a previous connection are optionally specified, to
+ * attempt session resumption. The instance handles the connection but
+ * cannot be "revived" after the connection was closed (the session
+ * parameters, though, can be extracted and used with another instance).
+ */
+
+public class SSLClient : SSLEngine {
+
+	/*
+	 * Create the client over the provided stream. No attempt at
+	 * session resumption will be made.
+	 */
+	public SSLClient(Stream sub) : this(sub, null)
+	{
+	}
+
+	/*
+	 * Create the client over the provided stream. If the
+	 * 'sessionParameters' are not null, then the client will try to
+	 * resume that session. Note that session parameters may include
+	 * a "target server name", in which case the ServerName
+	 * property will be set to that name, which will be included
+	 * in the ClientHello as the Server Name Indication extension.
+	 */
+	public SSLClient(Stream sub, SSLSessionParameters sessionParameters)
+		: base(sub)
+	{
+		sentExtensions = new List<int>();
+		resumeParams = sessionParameters;
+		if (resumeParams != null) {
+			string name = resumeParams.ServerName;
+			if (name != null) {
+				ServerName = name;
+			}
+		}
+	}
+
+	/*
+	 * Validator for the server certificate. This callback is
+	 * responsible for obtaining the server's public key and making
+	 * sure it is the right one. A normal, standard-compliant
+	 * implementation should do the following:
+	 *
+	 * -- Validate the certificate as X.509 mandates (building
+	 * a path to a trust anchor, and verifying all signatures, names
+	 * and appropriate certificate extensions; also obtaining
+	 * proper CRL or OCSP response for a fresh revocation status).
+	 *
+	 * -- Check that the intended server name (provided in the
+	 * 'serverName' parameter) matches that which is found in the
+	 * certificate (see RFC 2818 section 3.1 for details; also
+	 * consider RFC 6125 section 6.4).
+	 *
+	 * -- Return the public key found in the server's certificate,
+	 * along with its allowed usages (which may depend on the
+	 * KeyUsage extensions found in the certificate).
+	 *
+	 * The certificate chain, as received from the server, is
+	 * provided as parameter; it is non-empty (it contains at least
+	 * one certificate). The server's certificate is the first one
+	 * in the chain.
+	 *
+	 * The 'serverName' parameter is the intended server name, to
+	 * match against the names found in the certificate. If it is
+	 * null, then no matching is expected (this correspond to the
+	 * ServerName property in this SSLClient instance).
+	 *
+	 * The 'usage' variable shall be set to a value that qualifies
+	 * whether the key may be used for encryption and/or signatures.
+	 */
+	public delegate IPublicKey CertValidator(
+		byte[][] chain, string serverName, out KeyUsage usage);
+	public CertValidator ServerCertValidator {
+		get; set;
+	}
+
+	/*
+	 * A simple INSECURE certificate "validator" that does not validate
+	 * anything: the public key is extracted and returned, with no
+	 * other checks. THIS IS FOR TESTS ONLY. Using this validator
+	 * basically voids all security properties of SSL.
+	 */
+	public static IPublicKey InsecureCertValidator(
+		byte[][] chain, string serverName, out KeyUsage usage)
+	{
+		usage = KeyUsage.EncryptAndSign;
+		return SSL.GetKeyFromCert(chain[0]);
+	}
+
+	List<int> sentExtensions;
+	SSLSessionParameters resumeParams;
+
+	internal override bool IsClient {
+		get {
+			return true;
+		}
+	}
+
+	internal override bool DoHandshake()
+	{
+		CheckConfigHashAndSign();
+
+		ResetHashes();
+		MakeRandom(clientRandom);
+
+		SendClientHello();
+		FlushSub();
+
+		bool resume;
+		if (!ParseServerHello(out resume)) {
+			return false;
+		}
+		HandshakeCount ++;
+		if (resume) {
+			/*
+			 * Abbreviated handshake.
+			 */
+			ParseCCSAndFinished();
+			SendCCSAndFinished();
+			FlushSub();
+			SetAppData();
+			IsResume = true;
+			return true;
+		}
+
+		KeyUsage usage;
+		IPublicKey pkey = ParseCertificate(out usage);
+
+		ECCurve curve;
+		byte[] serverPoint;
+		if (SSL.IsECDHE_RSA(CipherSuite)) {
+			if (!(pkey is RSAPublicKey)) {
+				throw new SSLException(
+					"ECDHE_RSA needs a RSA public key");
+			}
+			if (usage != KeyUsage.SignOnly
+				&& usage != KeyUsage.EncryptAndSign)
+			{
+				throw new SSLException("Server public key"
+					+ " unfit for signatures");
+			}
+			serverPoint = ParseServerKeyExchange(out curve, pkey);
+		} else if (SSL.IsECDHE_ECDSA(CipherSuite)) {
+			if (!(pkey is ECPublicKey)) {
+				throw new SSLException(
+					"ECDHE_ECDSA needs an EC public key");
+			}
+			if (usage != KeyUsage.SignOnly
+				&& usage != KeyUsage.EncryptAndSign)
+			{
+				throw new SSLException("Server public key"
+					+ " unfit for signatures");
+			}
+			serverPoint = ParseServerKeyExchange(out curve, pkey);
+		} else {
+			curve = null;
+			serverPoint = null;
+		}
+
+		bool reqClientCert = false;
+		int mt;
+		byte[] msg = ReadHandshakeMessage(out mt);
+		if (mt == SSL.CERTIFICATE_REQUEST) {
+			/*
+			 * FIXME: parse message and select a client
+			 * certificate.
+			 */
+			reqClientCert = true;
+			msg = ReadHandshakeMessage(out mt);
+		}
+		if (mt != SSL.SERVER_HELLO_DONE) {
+			throw new SSLException(string.Format("Unexpected"
+				+ " handshake message {0}"
+				+ " (expected: ServerHelloDone)", mt));
+		}
+		if (msg.Length != 0) {
+			throw new SSLException(
+				"Invalid ServerHelloDone (not empty)");
+		}
+
+		if (reqClientCert) {
+			/*
+			 * FIXME: right now, we send an empty Certificate
+			 * message if the server asks for a client
+			 * certificate; i.e., we claim we have none.
+			 */
+			MemoryStream ms =
+				StartHandshakeMessage(SSL.CERTIFICATE);
+			EndHandshakeMessage(ms);
+		}
+
+		if (SSL.IsRSA(CipherSuite)) {
+			if (!(pkey is RSAPublicKey)) {
+				throw new SSLException(
+					"Server public key is not RSA");
+			}
+			if (usage != KeyUsage.EncryptOnly
+				&& usage != KeyUsage.EncryptAndSign)
+			{
+				throw new SSLException("Server public key is"
+					+ " not allowed for encryption");
+			}
+			SendClientKeyExchangeRSA(pkey as RSAPublicKey);
+		} else if (SSL.IsECDH(CipherSuite)) {
+			if (!(pkey is ECPublicKey)) {
+				throw new SSLException(
+					"Server public key is not EC");
+			}
+			if (usage != KeyUsage.EncryptOnly
+				&& usage != KeyUsage.EncryptAndSign)
+			{
+				throw new SSLException("Server public key is"
+					+ " not allowed for key exchange");
+			}
+			SendClientKeyExchangeECDH(pkey as ECPublicKey);
+		} else if (serverPoint != null) {
+			SendClientKeyExchangeECDH(curve, serverPoint);
+		} else {
+			/*
+			 * TODO: Maybe support DHE cipher suites?
+			 */
+			throw new Exception("NYI");
+		}
+
+		/*
+		 * FIXME: when client certificates are supported, we
+		 * will need to send a CertificateVerify message here.
+		 */
+
+		SendCCSAndFinished();
+		FlushSub();
+
+		ParseCCSAndFinished();
+		SetAppData();
+		IsResume = false;
+
+		return true;
+	}
+
+	void SendClientHello()
+	{
+		MemoryStream ms = StartHandshakeMessage(SSL.CLIENT_HELLO);
+
+		// Maximum supported protocol version.
+		IO.Write16(ms, VersionMax);
+
+		// Client random.
+		ms.Write(clientRandom, 0, clientRandom.Length);
+
+		// Session ID.
+		if (resumeParams != null) {
+			byte[] id = resumeParams.SessionID;
+			ms.WriteByte((byte)id.Length);
+			ms.Write(id, 0, id.Length);
+		} else {
+			ms.WriteByte(0x00);
+		}
+
+		// List of supported cipher suites.
+		int csLen = SupportedCipherSuites.Length << 1;
+		int extraCS = GetQuirkInt("sendExtraCipherSuite", -1);
+		if (extraCS >= 0) {
+			csLen += 2;
+		}
+		IO.Write16(ms, csLen);
+		foreach (int cs in SupportedCipherSuites) {
+			IO.Write16(ms, cs);
+		}
+		if (extraCS >= 0) {
+			IO.Write16(ms, extraCS);
+		}
+
+		// List of supported compression algorithms.
+		ms.WriteByte(0x01);
+		ms.WriteByte(0x00);
+
+		// Extensions
+		sentExtensions.Clear();
+		MemoryStream chExt = new MemoryStream();
+
+		// Server Name Indication
+		if (ServerName != null && ServerName.Length > 0) {
+			byte[] encName = Encoding.UTF8.GetBytes(ServerName);
+			int elen = encName.Length;
+			if (elen > 65530) {
+				throw new SSLException("Oversized server name");
+			}
+			sentExtensions.Add(0x0000);
+			IO.Write16(chExt, 0x0000);    // extension type
+			IO.Write16(chExt, elen + 5);  // extension length
+			IO.Write16(chExt, elen + 3);  // name list length
+			chExt.WriteByte(0x00);        // name type
+			IO.Write16(chExt, elen);      // name length
+			chExt.Write(encName, 0, elen);
+		}
+
+		// Supported Curves and Supported Point Formats
+		if (SupportedCurves != null && SupportedCurves.Length > 0) {
+			int len = SupportedCurves.Length;
+			sentExtensions.Add(0x000A);
+			IO.Write16(chExt, 0x000A);
+			IO.Write16(chExt, (len << 1) + 2);
+			IO.Write16(chExt, len << 1);
+			foreach (int cc in SupportedCurves) {
+				IO.Write16(chExt, cc);
+			}
+
+			sentExtensions.Add(0x000B);
+			IO.Write16(chExt, 0x000B);
+			IO.Write16(chExt, 2);
+			chExt.WriteByte(1);
+			chExt.WriteByte(0x00);
+		}
+
+		// Supported Signatures
+		if (VersionMax >= SSL.TLS12 && SupportedHashAndSign != null
+			&& SupportedHashAndSign.Length > 0)
+		{
+			sentExtensions.Add(0x000D);
+			IO.Write16(chExt, 0x000D);
+			int num = SupportedHashAndSign.Length;
+			IO.Write16(chExt, 2 + (num << 1));
+			IO.Write16(chExt, num << 1);
+			foreach (int hs in SupportedHashAndSign) {
+				IO.Write16(chExt, hs);
+			}
+		}
+
+		// Secure renegotiation
+		if (!GetQuirkBool("noSecureReneg")) {
+			sentExtensions.Add(0xFF01);
+			IO.Write16(chExt, 0xFF01);
+			byte[] exv;
+			if (renegSupport > 0) {
+				exv = savedClientFinished;
+			} else {
+				exv = new byte[0];
+			}
+
+			if (GetQuirkBool("forceEmptySecureReneg")) {
+				exv = new byte[0];
+			} else if (GetQuirkBool("forceNonEmptySecureReneg")) {
+				exv = new byte[12];
+			} else if (GetQuirkBool("alterNonEmptySecureReneg")) {
+				if (exv.Length > 0) {
+					exv[exv.Length - 1] ^= 0x01;
+				}
+			} else if (GetQuirkBool("oversizedSecureReneg")) {
+				exv = new byte[255];
+			}
+
+			IO.Write16(chExt, exv.Length + 1);
+			chExt.WriteByte((byte)exv.Length);
+			chExt.Write(exv, 0, exv.Length);
+		}
+
+		// Extra extension with random contents.
+		int extraExt = GetQuirkInt("sendExtraExtension", -1);
+		if (extraExt >= 0) {
+			byte[] exv = new byte[extraExt >> 16];
+			RNG.GetBytes(exv);
+			IO.Write16(chExt, extraExt & 0xFFFF);
+			IO.Write16(chExt, exv.Length);
+			chExt.Write(exv, 0, exv.Length);
+		}
+
+		// Max Fragment Length
+		// ALPN
+		// FIXME
+
+		byte[] encExt = chExt.ToArray();
+		if (encExt.Length > 0) {
+			if (encExt.Length > 65535) {
+				throw new SSLException("Oversized extensions");
+			}
+			IO.Write16(ms, encExt.Length);
+			ms.Write(encExt, 0, encExt.Length);
+		}
+
+		EndHandshakeMessage(ms);
+	}
+
+	bool ParseServerHello(out bool resume)
+	{
+		resume = false;
+
+		int mt;
+		byte[] msg = ReadHandshakeMessage(out mt, FirstHandshakeDone);
+		if (msg == null) {
+			/*
+			 * Server denies attempt explicitly.
+			 */
+			return false;
+		}
+		if (mt != SSL.SERVER_HELLO) {
+			throw new SSLException(string.Format("Unexpected"
+				+ " handshake message {0} (expecting a"
+				+ " ServerHello)", mt));
+		}
+
+		if (msg.Length < 38) {
+			throw new SSLException("Truncated ServerHello");
+		}
+		Version = IO.Dec16be(msg, 0);
+		if (Version < VersionMin || Version > VersionMax) {
+			throw new SSLException(string.Format(
+				"Unsupported version: 0x{0:X4}", Version));
+		}
+		Array.Copy(msg, 2, serverRandom, 0, 32);
+		int idLen = msg[34];
+		if (idLen > 32) {
+			throw new SSLException("Invalid session ID length");
+		}
+		if (idLen + 38 > msg.Length) {
+			throw new SSLException("Truncated ServerHello");
+		}
+		sessionID = new byte[idLen];
+		Array.Copy(msg, 35, sessionID, 0, idLen);
+		int off = 35 + idLen;
+
+		/*
+		 * Cipher suite. It must be one of the suites we sent.
+		 */
+		CipherSuite = IO.Dec16be(msg, off);
+		off += 2;
+		bool found = false;
+		foreach (int cs in SupportedCipherSuites) {
+			if (cs == SSL.FALLBACK_SCSV
+				|| cs == SSL.EMPTY_RENEGOTIATION_INFO_SCSV)
+			{
+				continue;
+			}
+			if (cs == CipherSuite) {
+				found = true;
+				break;
+			}
+		}
+		if (!found) {
+			throw new SSLException(string.Format(
+				"Server selected cipher suite 0x{0:X4}"
+				+ " which we did not advertise", CipherSuite));
+		}
+
+		/*
+		 * Compression. Must be 0, since we do not support it.
+		 */
+		int comp = msg[off ++];
+		if (comp != 0x00) {
+			throw new SSLException(string.Format(
+				"Server selected compression {0}"
+				+ " which we did not advertise", comp));
+		}
+
+		/*
+		 * Extensions. Each extension from the server should
+		 * correspond to an extension sent in the ClientHello.
+		 */
+		bool secReneg = false;
+		if (msg.Length > off) {
+			if (msg.Length == off + 1) {
+				throw new SSLException("Truncated ServerHello");
+			}
+			int tlen = IO.Dec16be(msg, off);
+			off += 2;
+			if (tlen != msg.Length - off) {
+				throw new SSLException(
+					"Invalid extension list length");
+			}
+			while (off < msg.Length) {
+				if ((off + 4) > msg.Length) {
+					throw new SSLException(
+						"Truncated extention");
+				}
+				int etype = IO.Dec16be(msg, off);
+				int elen = IO.Dec16be(msg, off + 2);
+				off += 4;
+				if (elen > msg.Length - off) {
+					throw new SSLException(
+						"Truncated extention");
+				}
+				if (!sentExtensions.Contains(etype)) {
+					throw new SSLException(string.Format(
+						"Server send unadvertised"
+						+ " extenstion 0x{0:X4}",
+						etype));
+				}
+
+				/*
+				 * We have some processing to do on some
+				 * specific server-side extensions.
+				 */
+				switch (etype) {
+
+				case 0xFF01:
+					secReneg = true;
+					ParseExtSecureReneg(msg, off, elen);
+					break;
+				}
+
+				off += elen;
+			}
+		}
+
+		/*
+		 * Renegotiation support: if we did not get the extension
+		 * from the server, then secure renegotiation is
+		 * definitely not supported. If it _was_ known as
+		 * being supported (from a previous handshake) then this
+		 * is a fatal error.
+		 */
+		if (!secReneg) {
+			if (renegSupport > 0) {
+				throw new SSLException("Missing Secure"
+					+ " Renegotiation extension");
+			}
+			renegSupport = -1;
+		}
+
+		/*
+		 * Check whether this is a session resumption: a session
+		 * is resumed if we sent a non-empty session ID, and the
+		 * ServerHello contained the same session ID.
+		 *
+		 * In case of resumption, the ServerHello must use the
+		 * same version and cipher suite than in the saved
+		 * parameters.
+		 */
+		if (resumeParams != null) {
+			byte[] id = resumeParams.SessionID;
+			if (id.Length > 0 && IO.Eq(id, sessionID)) {
+				if (Version != resumeParams.Version) {
+					throw new SSLException(
+						"Resume version mismatch");
+				}
+				if (CipherSuite != resumeParams.CipherSuite) {
+					throw new SSLException(
+						"Resume cipher suite mismatch");
+				}
+				SetMasterSecret(resumeParams.MasterSecret);
+				resume = true;
+			}
+		}
+
+		return true;
+	}
+
+	void ParseExtSecureReneg(byte[] buf, int off, int len)
+	{
+		if (len < 1 || len != 1 + buf[off]) {
+			throw new SSLException(
+				"Invalid Secure Renegotiation extension");
+		}
+		len --;
+		off ++;
+
+		if (renegSupport == 0) {
+			/*
+			 * Initial handshake: extension MUST be empty.
+			 */
+			if (len != 0) {
+				throw new SSLException(
+					"Non-empty Secure Renegotation"
+					+ " on initial handshake");
+			}
+			renegSupport = 1;
+		} else {
+			/*
+			 * Renegotiation: extension MUST contain the
+			 * concatenation of the saved client and
+			 * server Finished messages (in that order).
+			 */
+			if (len != 24) {
+				throw new SSLException(
+					"Wrong Secure Renegotiation value");
+			}
+			int z = 0;
+			for (int i = 0; i < 12; i ++) {
+				z |= savedClientFinished[i] ^ buf[off + i];
+				z |= savedServerFinished[i] ^ buf[off + 12 + i];
+			}
+			if (z != 0) {
+				throw new SSLException(
+					"Wrong Secure Renegotiation value");
+			}
+		}
+	}
+
+	IPublicKey ParseCertificate(out KeyUsage usage)
+	{
+		byte[] msg = ReadHandshakeMessageExpected(SSL.CERTIFICATE);
+		if (msg.Length < 3) {
+			throw new SSLException("Invalid Certificate message");
+		}
+		int tlen = IO.Dec24be(msg, 0);
+		int off = 3;
+		if (tlen != msg.Length - off) {
+			throw new SSLException("Invalid Certificate message");
+		}
+		List<byte[]> certs = new List<byte[]>();
+		while (off < msg.Length) {
+			if (msg.Length - off < 3) {
+				throw new SSLException(
+					"Invalid Certificate message");
+			}
+			int clen = IO.Dec24be(msg, off);
+			off += 3;
+			if (clen > msg.Length - off) {
+				throw new SSLException(
+					"Invalid Certificate message");
+			}
+			byte[] ec = new byte[clen];
+			Array.Copy(msg, off, ec, 0, clen);
+			off += clen;
+			certs.Add(ec);
+		}
+
+		return ServerCertValidator(
+			certs.ToArray(), ServerName, out usage);
+	}
+
+	byte[] ParseServerKeyExchange(out ECCurve curve, IPublicKey pkey)
+	{
+		byte[] msg = ReadHandshakeMessageExpected(
+			SSL.SERVER_KEY_EXCHANGE);
+		if (msg.Length < 4) {
+			throw new SSLException(
+				"Invalid ServerKeyExchange message");
+		}
+		if (msg[0] != 0x03) {
+			throw new SSLException("Unsupported unnamed curve");
+		}
+		curve = SSL.GetCurveByID(IO.Dec16be(msg, 1));
+		int plen = msg[3];
+		int off = 4;
+		if (msg.Length - off < plen) {
+			throw new SSLException(
+				"Invalid ServerKeyExchange message");
+		}
+		byte[] point = new byte[plen];
+		Array.Copy(msg, off, point, 0, plen);
+		off += plen;
+		int slen = off;
+
+		int hashId, sigId;
+		if (Version >= SSL.TLS12) {
+			if (msg.Length - off < 2) {
+				throw new SSLException(
+					"Invalid ServerKeyExchange message");
+			}
+			hashId = msg[off ++];
+			if (hashId == 0) {
+				throw new SSLException(
+					"Invalid hash identifier");
+			}
+			sigId = msg[off ++];
+		} else {
+			if (pkey is RSAPublicKey) {
+				hashId = 0;
+				sigId = 1;
+			} else if (pkey is ECPublicKey) {
+				hashId = 2;
+				sigId = 3;
+			} else {
+				throw new SSLException(
+					"Unsupported signature key type");
+			}
+		}
+		
+		if (msg.Length - off < 2) {
+			throw new SSLException(
+				"Invalid ServerKeyExchange message");
+		}
+		int sigLen = IO.Dec16be(msg, off);
+		off += 2;
+		if (sigLen != msg.Length - off) {
+			throw new SSLException(
+				"Invalid ServerKeyExchange message");
+		}
+		byte[] sig = new byte[sigLen];
+		Array.Copy(msg, off, sig, 0, sigLen);
+
+		byte[] hv;
+		if (hashId == 0) {
+			MD5 md5 = new MD5();
+			SHA1 sha1 = new SHA1();
+			md5.Update(clientRandom);
+			md5.Update(serverRandom);
+			md5.Update(msg, 0, slen);
+			sha1.Update(clientRandom);
+			sha1.Update(serverRandom);
+			sha1.Update(msg, 0, slen);
+			hv = new byte[36];
+			md5.DoFinal(hv, 0);
+			sha1.DoFinal(hv, 16);
+		} else {
+			IDigest h = SSL.GetHashByID(hashId);
+			h.Update(clientRandom);
+			h.Update(serverRandom);
+			h.Update(msg, 0, slen);
+			hv = h.DoFinal();
+		}
+
+		bool ok;
+		if (sigId == 1) {
+			RSAPublicKey rpk = pkey as RSAPublicKey;
+			if (rpk == null) {
+				throw new SSLException(
+					"Wrong public key type for RSA");
+			}
+			if (hashId == 0) {
+				ok = RSA.VerifyND(rpk, hv, sig);
+			} else {
+				byte[] head1, head2;
+
+				switch (hashId) {
+				case 1:
+					head1 = RSA.PKCS1_MD5;
+					head2 = RSA.PKCS1_MD5_ALT;
+					break;
+				case 2:
+					head1 = RSA.PKCS1_SHA1;
+					head2 = RSA.PKCS1_SHA1_ALT;
+					break;
+				case 3:
+					head1 = RSA.PKCS1_SHA224;
+					head2 = RSA.PKCS1_SHA224_ALT;
+					break;
+				case 4:
+					head1 = RSA.PKCS1_SHA256;
+					head2 = RSA.PKCS1_SHA256_ALT;
+					break;
+				case 5:
+					head1 = RSA.PKCS1_SHA384;
+					head2 = RSA.PKCS1_SHA384_ALT;
+					break;
+				case 6:
+					head1 = RSA.PKCS1_SHA512;
+					head2 = RSA.PKCS1_SHA512_ALT;
+					break;
+				default:
+					throw new SSLException(
+						"Unsupported hash algorithm: "
+						+ hashId);
+				}
+				ok = RSA.Verify(rpk, head1, head2, hv, sig);
+			}
+		} else if (sigId == 3) {
+			ECPublicKey epk = pkey as ECPublicKey;
+			if (epk == null) {
+				throw new SSLException(
+					"Wrong public key type for ECDSA");
+			}
+			ok = ECDSA.Verify(epk, hv, sig);
+		} else {
+			throw new SSLException(
+				"Unsupported signature type: " + sigId);
+		}
+
+		if (!ok) {
+			throw new SSLException(
+				"Invalid signature on ServerKeyExchange");
+		}
+		return point;
+	}
+
+	void SendClientKeyExchangeRSA(RSAPublicKey pkey)
+	{
+		byte[] pms = new byte[48];
+		IO.Enc16be(Version, pms, 0);
+		RNG.GetBytes(pms, 2, pms.Length - 2);
+		byte[] epms = RSA.Encrypt(pkey, pms);
+		MemoryStream ms =
+			StartHandshakeMessage(SSL.CLIENT_KEY_EXCHANGE);
+		IO.Write16(ms, epms.Length);
+		ms.Write(epms, 0, epms.Length);
+		EndHandshakeMessage(ms);
+		ComputeMaster(pms);
+	}
+
+	void SendClientKeyExchangeECDH(ECPublicKey pkey)
+	{
+		ECCurve curve = pkey.Curve;
+		SendClientKeyExchangeECDH(curve, pkey.Pub);
+	}
+
+	void SendClientKeyExchangeECDH(ECCurve curve, byte[] pub)
+	{
+		byte[] k = curve.MakeRandomSecret();
+
+		/*
+		 * Compute the point P = k*G that we will send.
+		 */
+		byte[] P = curve.GetGenerator(false);
+		uint good = curve.Mul(P, k, P, false);
+
+		/*
+		 * Compute the shared secret Q = k*Pub.
+		 */
+		byte[] Q = new byte[P.Length];
+		good &= curve.Mul(pub, k, Q, false);
+
+		if (good == 0) {
+			/*
+			 * This might happen only if the server's public
+			 * key is not part of the proper subgroup. This
+			 * cannot happen with NIST's "P" curves.
+			 */
+			throw new SSLException("ECDH failed");
+		}
+
+		/*
+		 * Send message.
+		 */
+		MemoryStream ms =
+			StartHandshakeMessage(SSL.CLIENT_KEY_EXCHANGE);
+		ms.WriteByte((byte)P.Length);
+		ms.Write(P, 0, P.Length);
+		EndHandshakeMessage(ms);
+
+		/*
+		 * Extract premaster secret.
+		 */
+		int xlen;
+		int xoff = curve.GetXoff(out xlen);
+		byte[] pms = new byte[xlen];
+		Array.Copy(Q, xoff, pms, 0, xlen);
+		ComputeMaster(pms);
+	}
+
+	internal override void ProcessExtraHandshake()
+	{
+		/*
+		 * If we receive a non-empty handshake message, then
+		 * it should be an HelloRequest. Note that we expect
+		 * the request not to be mixed with application data
+		 * records (though it could be split).
+		 */
+		ReadHelloRequests();
+
+		/*
+		 * We accept to renegotiate only if the server supports
+		 * the Secure Renegotiation extension.
+		 */
+		if (renegSupport != 1) {
+			SendWarning(SSL.NO_RENEGOTIATION);
+			SetAppData();
+			return;
+		}
+
+		/*
+		 * We do a new handshake. We explicitly refuse to reuse
+		 * session parameters, because there is no point in
+		 * renegotiation if this resumes the same session.
+		 */
+		resumeParams = null;
+		DoHandshake();
+	}
+
+	internal override void PrepareRenegotiate()
+	{
+		/*
+		 * Nothing to do to trigger a new handshake, the client
+		 * just has to send the ClientHello right away.
+		 */
+	}
+}
+
+}
diff --git a/SSLTLS/SSLEngine.cs b/SSLTLS/SSLEngine.cs
new file mode 100644
index 0000000..e4ebf95
--- /dev/null
+++ b/SSLTLS/SSLEngine.cs
@@ -0,0 +1,1688 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+using Crypto;
+
+namespace SSLTLS {
+
+/*
+ * This is the base class common to SSLClient and SSLServer.
+ */
+
+public abstract class SSLEngine : Stream {
+
+	Stream sub;
+	InputRecord inRec;
+	OutputRecord outRec;
+	int deferredAlert;
+	int state;
+	int versionMin;
+	int actualVersion;
+	bool receivedNoReneg;
+
+	/*
+	 * Functions used to hash handshake messages.
+	 */
+	MD5 md5;
+	SHA1 sha1;
+	SHA256 sha256;
+	SHA384 sha384;
+
+	/*
+	 * State:
+	 *   STATE_HANDSHAKE   expecting handshake message only
+	 *   STATE_CCS         expecting Change Cipher Spec message only
+	 *   STATE_APPDATA     expecting application data or handshake
+	 *   STATE_CLOSING     expecting only alert (close_notify)
+	 *   STATE_CLOSED      closed
+	 */
+	internal const int STATE_CLOSED    = 0;
+	internal const int STATE_HANDSHAKE = 1;
+	internal const int STATE_CCS       = 2;
+	internal const int STATE_APPDATA   = 3;
+	internal const int STATE_CLOSING   = 4;
+
+	/*
+	 * Default cipher suites.
+	 */
+	static int[] DEFAULT_CIPHER_SUITES = {
+		SSL.ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
+		SSL.ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
+
+		SSL.ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
+		SSL.ECDHE_RSA_WITH_AES_128_GCM_SHA256,
+		SSL.ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
+		SSL.ECDHE_RSA_WITH_AES_256_GCM_SHA384,
+		SSL.ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
+		SSL.ECDHE_RSA_WITH_AES_128_CBC_SHA256,
+		SSL.ECDHE_ECDSA_WITH_AES_256_CBC_SHA384,
+		SSL.ECDHE_RSA_WITH_AES_256_CBC_SHA384,
+		SSL.ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
+		SSL.ECDHE_RSA_WITH_AES_128_CBC_SHA,
+		SSL.ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
+		SSL.ECDHE_RSA_WITH_AES_256_CBC_SHA,
+
+		SSL.ECDH_ECDSA_WITH_AES_128_GCM_SHA256,
+		SSL.ECDH_RSA_WITH_AES_128_GCM_SHA256,
+		SSL.ECDH_ECDSA_WITH_AES_256_GCM_SHA384,
+		SSL.ECDH_RSA_WITH_AES_256_GCM_SHA384,
+		SSL.ECDH_ECDSA_WITH_AES_128_CBC_SHA256,
+		SSL.ECDH_RSA_WITH_AES_128_CBC_SHA256,
+		SSL.ECDH_ECDSA_WITH_AES_256_CBC_SHA384,
+		SSL.ECDH_RSA_WITH_AES_256_CBC_SHA384,
+		SSL.ECDH_ECDSA_WITH_AES_128_CBC_SHA,
+		SSL.ECDH_RSA_WITH_AES_128_CBC_SHA,
+		SSL.ECDH_ECDSA_WITH_AES_256_CBC_SHA,
+		SSL.ECDH_RSA_WITH_AES_256_CBC_SHA,
+
+		SSL.RSA_WITH_AES_128_GCM_SHA256,
+		SSL.RSA_WITH_AES_256_GCM_SHA384,
+		SSL.RSA_WITH_AES_128_CBC_SHA256,
+		SSL.RSA_WITH_AES_256_CBC_SHA256,
+		SSL.RSA_WITH_AES_128_CBC_SHA,
+		SSL.RSA_WITH_AES_256_CBC_SHA,
+
+		SSL.ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA,
+		SSL.ECDHE_RSA_WITH_3DES_EDE_CBC_SHA,
+		SSL.ECDH_RSA_WITH_3DES_EDE_CBC_SHA,
+		SSL.ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA,
+		SSL.RSA_WITH_3DES_EDE_CBC_SHA
+	};
+
+	/*
+	 * Default curves.
+	 */
+	static int[] DEFAULT_CURVES = {
+		SSL.Curve25519,
+		SSL.NIST_P256,
+		SSL.NIST_P384,
+		SSL.NIST_P521
+	};
+
+	/*
+	 * Default hash and sign algorithms.
+	 */
+	static int[] DEFAULT_HASHANDSIGN = {
+		SSL.ECDSA_SHA256,
+		SSL.RSA_SHA256,
+		SSL.ECDSA_SHA224,
+		SSL.RSA_SHA224,
+		SSL.ECDSA_SHA384,
+		SSL.RSA_SHA384,
+		SSL.ECDSA_SHA512,
+		SSL.RSA_SHA512,
+		SSL.ECDSA_SHA1,
+		SSL.RSA_SHA1
+	};
+
+	internal byte[] clientRandom;
+	internal byte[] serverRandom;
+	internal byte[] sessionID;
+
+	/*
+	 * 'renegSupport' is one of:
+	 *    0   initial handshake not done yet
+	 *    1   peer supports secure renegotiation
+	 *   -1   peer does not support secure renegotiation
+	 */
+	internal int renegSupport;
+	internal byte[] savedClientFinished;
+	internal byte[] savedServerFinished;
+
+	byte[] masterSecret;
+
+	/*
+	 * Create a new engine over the provided transport stream.
+	 */
+	public SSLEngine(Stream sub)
+	{
+		this.sub = sub;
+		inRec = new InputRecord(sub);
+		outRec = new OutputRecord(sub);
+		md5 = new MD5();
+		sha1 = new SHA1();
+		sha256 = new SHA256();
+		sha384 = new SHA384();
+		state = STATE_HANDSHAKE;
+		deferredAlert = -1;
+		AutoFlush = true;
+		CloseSub = true;
+		OnClose = null;
+		NoCloseNotify = false;
+		ClosedWithoutNotify = false;
+		MaximumHandshakeMessageLength = 65536;
+		SupportedCipherSuites = DEFAULT_CIPHER_SUITES;
+		SupportedCurves = DEFAULT_CURVES;
+		SupportedHashAndSign = DEFAULT_HASHANDSIGN;
+		VersionMin = SSL.TLS10;
+		VersionMax = SSL.TLS12;
+		AllowRenegotiation = true;
+		receivedNoReneg = false;
+		clientRandom = new byte[32];
+		serverRandom = new byte[32];
+		masterSecret = new byte[48];
+		HandshakeCount = 0;
+	}
+
+	/*
+	 * If 'NormalizeIOError' is true, then I/O errors while writing
+	 * on the underlying stream will be reported as a generic
+	 * SSLException with message "Unexpected transport closure".
+	 * This helps test code that expects an asynchronous abort that
+	 * may be detected during a read of a write operation, depending
+	 * on the exact timing. Default is false.
+	 */
+	public bool NormalizeIOError {
+		get {
+			return outRec.NormalizeIOError;
+		}
+		set {
+			outRec.NormalizeIOError = value;
+		}
+	}
+
+	/*
+	 * If 'AutoFlush' is true, then after every Write() or WriteByte()
+	 * call, the current record is assembled and sent, leaving no
+	 * buffered data yet to be sent. Default value is true.
+	 */
+	public bool AutoFlush {
+		get; set;
+	}
+
+	/*
+	 * If 'CloseSub' is true, then the underlying transport stream
+	 * will be closed on normal closure or protocol failure. Default
+	 * value is true. If a closure callback is set in 'OnClose',
+	 * then this flag is ignored.
+	 */
+	public bool CloseSub {
+		get; set;
+	}
+
+	/*
+	 * A generic callback to be invoked when the SSL connection is
+	 * closed. If a callback is specified here, then the 'CloseSub'
+	 * flag is ignored; the callback is supposed to handle the
+	 * closing of the transport stream, if necessary.
+	 */
+	public delegate void CloseGen(Stream sub);
+	public CloseGen OnClose {
+		get; set;
+	}
+
+	/*
+	 * If 'NoCloseNotify' is true, then lack of a close_notify from
+	 * the peer before closing the transport stream will NOT be
+	 * considered erroneous; i.e. it won't trigger an exception.
+	 * If that situation arises, the ClosedWithoutNotify flag will
+	 * return true, so the caller may still test for it.
+	 *
+	 * Not sending close_notify alerts before closing is a widespread
+	 * practice, since it simplifies timeout management. It is unsafe,
+	 * in that it allows truncation attacks, unless the application
+	 * protocol is self-terminated (e.g. HTTP/1.1 is self-terminated,
+	 * HTTP/0.9 is not).
+	 *
+	 * Default value is false: lack of close_notify triggers an
+	 * exception.
+	 */
+	public bool NoCloseNotify {
+		get; set;
+	}
+
+	/*
+	 * The 'ClosedWithoutNotify' flag is set to true if the connection
+	 * was closed abruptly (no close_notify alert), but this engine
+	 * was configured to tolerate that situation ('NoCloseNotify' was
+	 * set to true).
+	 */
+	public bool ClosedWithoutNotify {
+		get; private set;
+	}
+
+	/*
+	 * Maximum allowed size for a handshake message. The protocol
+	 * allows for messages up to 16 megabytes, but these hardly
+	 * make sense in practice. In the interest of avoiding
+	 * memory-based denials of service, a lower limit can be set.
+	 * If an incoming handshake message exceeds that size, then
+	 * an exception is thrown. Default value is 65536.
+	 */
+	public int MaximumHandshakeMessageLength {
+		get; set;
+	}
+
+	/*
+	 * Set the cipher suites supported by this engine, in preference
+	 * order (most preferred comes first). Default value should ensure
+	 * maximum interoperability and good security and performance.
+	 */
+	public int[] SupportedCipherSuites {
+		get; set;
+	}
+
+	/*
+	 * Set the elliptic curves supported by this engine, for ECDH
+	 * and ECDHE. The list is in preference order (most preferred
+	 * comes first). Default list is Curve25519 followed by the
+	 * usual NIST curves (P-256, P-384 and P-521, in that order).
+	 */
+	public int[] SupportedCurves {
+		get; set;
+	}
+
+	/*
+	 * Set the supported hash and sign algorithm combinations. Each
+	 * value is a 16-bit integer: high byte is the hash algorithm
+	 * identifier, while low byte is the signature algorithm identifier.
+	 * List is ordered by preference order. Default list applies the
+	 * following rules:
+	 *
+	 *  - Hash function order is:
+	 *    SHA-256, SHA-224, SHA-384, SHA-512, SHA-1
+	 *
+	 *  - For the same hash function, ECDSA is preferred over RSA.
+	 *
+	 * Note that the special RSA with MD5+SHA-1 shall not be part of
+	 * this list. Its use is implicit when using TLS 1.0 or 1.1 with
+	 * a cipher suite or client authentication that uses RSA signatures.
+	 */
+	public int[] SupportedHashAndSign {
+		get; set;
+	}
+
+	/*
+	 * Set the minimum supported protocol version. Default is TLS 1.0.
+	 */
+	public int VersionMin {
+		get {
+			return versionMin;
+		}
+		set {
+			SetOutputRecordVersion(value);
+			versionMin = value;
+		}
+	}
+
+	/*
+	 * Set the maximum supported protocol version. Default is TLS 1.2.
+	 */
+	public int VersionMax {
+		get; set;
+	}
+
+	/*
+	 * Get the actual protocol version. This is set only when the
+	 * version is chosen, within the handshake.
+	 */
+	public int Version {
+		get {
+			return actualVersion;
+		}
+		internal set {
+			actualVersion = value;
+			SetOutputRecordVersion(value);
+		}
+	}
+
+	/*
+	 * Get the actual cipher suite. This is set only when it is chosen,
+	 * within the handshake.
+	 */
+	public int CipherSuite {
+		get; internal set;
+	}
+
+	/*
+	 * Get or set the server name associated with this connection.
+	 *
+	 * On a client, the caller shall set that name; if non-null and
+	 * non-empty, then it will be sent to the server as a SNI
+	 * extension; it will moreover be matched against the names
+	 * found in the server's certificate.
+	 *
+	 * On a server, this value is set to the host name received from
+	 * the client as a SNI extension (if any).
+	 */
+	public string ServerName {
+		get; set;
+	}
+
+	/*
+	 * Get the current session parameters. If the initial handshake
+	 * was not performed yet, then the handshake is performed now
+	 * (thus, this call may trigger an exception in case of I/O or
+	 * protocol error).
+	 *
+	 * The returned object is a freshly allocated copy, which is not
+	 * impacted by further activity on this engine.
+	 */
+	public SSLSessionParameters SessionParameters {
+		get {
+			if (state == STATE_HANDSHAKE) {
+				DoHandshakeWrapper();
+			}
+			return new SSLSessionParameters(sessionID, Version,
+				CipherSuite, ServerName, masterSecret);
+		}
+	}
+
+	/*
+	 * Renegotiation support: if true, then renegotiations will be
+	 * accepted (both explicit calls to Renegotiate(), and requests
+	 * from the peer); if false, then all renegotiation attempts will
+	 * be rejected.
+	 *
+	 * Default value is true. Regardless of the value of this flag,
+	 * renegotiation attempts will be denied if the peer does not
+	 * support the "Secure Renegotiation" extension (RFC 5746).
+	 */
+	public bool AllowRenegotiation {
+		get; set;
+	}
+
+	/*
+	 * Set "quirks" to alter engine behaviour. When null (which is the
+	 * default), normal behaviour occurs.
+	 */
+	public SSLQuirks Quirks {
+		get; set;
+	}
+
+	/*
+	 * Get the current handshake count. This starts at 0 (before
+	 * the initial handshake) and is incremented for each handshake.
+	 * The increment occurs at the start of the handshake (after
+	 * confirmation that a handshake will be indeed attempted, when
+	 * doing a renegotiation).
+	 */
+	public long HandshakeCount {
+		get; internal set;
+	}
+
+	/*
+	 * Tell whether the last handshake was a session resumption or not.
+	 * This flag is set at the end of the handshake.
+	 */
+	public bool IsResume {
+		get; internal set;
+	}
+
+	/*
+	 * Trigger a new handshake. If the initial handshake was not done
+	 * yet, then it is performed at that point. Otherwise, this is
+	 * a renegotiation attempt.
+	 *
+	 * Returned value is true if a new handshake happened, false
+	 * otherwise. For the initial handshake, true is always returned
+	 * (handshake failures trigger exceptions). For a renegotiation,
+	 * a 'false' value may be returned if one of the following holds:
+	 *  - This engine was configured not to use renegotiations.
+	 *  - The peer does not support secure renegotiation.
+	 *  - A renegotiation was attempted, but denied by the peer.
+	 */
+	public bool Renegotiate()
+	{
+		if (!FirstHandshakeDone) {
+			DoHandshakeWrapper();
+			return true;
+		}
+
+		if (!AllowRenegotiation) {
+			return false;
+		}
+		if (!GetQuirkBool("noSecureReneg") && renegSupport < 0) {
+			return false;
+		}
+		int rt = outRec.RecordType;
+		try {
+			PrepareRenegotiate();
+		} catch {
+			MarkFailed();
+			throw;
+		}
+		if (!DoHandshakeWrapper()) {
+			outRec.RecordType = rt;
+			return false;
+		}
+		return true;
+	}
+
+	/* ============================================================ */
+	/*
+	 * Stream standard API.
+	 */
+
+	public override int ReadByte()
+	{
+		if (state == STATE_CLOSED) {
+			return -1;
+		}
+		CheckAppData();
+		try {
+			return ZRead();
+		} catch {
+			MarkFailed();
+			throw;
+		}
+	}
+
+	public override int Read(byte[] buf, int off, int len)
+	{
+		if (state == STATE_CLOSED) {
+			return -1;
+		}
+		CheckAppData();
+		try {
+			return ZRead(buf, off, len);
+		} catch {
+			MarkFailed();
+			throw;
+		}
+	}
+
+	public override void WriteByte(byte x)
+	{
+		CheckAppData();
+		try {
+			outRec.Write(x);
+			if (AutoFlush) {
+				outRec.Flush();
+			}
+		} catch {
+			MarkFailed();
+			throw;
+		}
+	}
+
+	public override void Write(byte[] buf, int off, int len)
+	{
+		CheckAppData();
+		try {
+			outRec.Write(buf, off, len);
+			if (AutoFlush) {
+				outRec.Flush();
+			}
+		} catch {
+			MarkFailed();
+			throw;
+		}
+	}
+
+	public override void Flush()
+	{
+		CheckAppData();
+		try {
+			outRec.Flush();
+		} catch {
+			MarkFailed();
+			throw;
+		}
+	}
+
+	public override void Close()
+	{
+		Close(true);
+	}
+
+	void Close(bool expectCloseNotify)
+	{
+		if (state == STATE_CLOSED) {
+			return;
+		}
+		try {
+			if (state == STATE_APPDATA) {
+				SendWarning(SSL.CLOSE_NOTIFY);
+				state = STATE_CLOSING;
+				if (expectCloseNotify) {
+					if (!NextRecord()) {
+						return;
+					}
+					throw new SSLException(
+						"Peer does not want to close");
+				}
+			}
+		} catch {
+			// ignored
+		} finally {
+			MarkFailed();
+		}
+	}
+
+	public override long Seek(long off, SeekOrigin origin)
+	{
+		throw new NotSupportedException();
+	}
+
+	public override void SetLength(long len)
+	{
+		throw new NotSupportedException();
+	}
+
+	public override bool CanRead {
+		get {
+			return state != STATE_CLOSED;
+		}
+	}
+
+	public override bool CanWrite {
+		get {
+			return state != STATE_CLOSED;
+		}
+	}
+
+	public override bool CanSeek {
+		get {
+			return false;
+		}
+	}
+
+	public override long Length {
+		get {
+			throw new NotSupportedException();
+		}
+	}
+
+	public override long Position {
+		get {
+			throw new NotSupportedException();
+		}
+		set {
+			throw new NotSupportedException();
+		}
+	}
+
+	/* ============================================================ */
+
+	/*
+	 * Test whether this engine is a client or a server.
+	 */
+	internal abstract bool IsClient {
+		get;
+	}
+
+	/*
+	 * Test the configuration of hash-and-sign with regards to
+	 * cipher suites: if the list of cipher suites includes an
+	 * ECDHE suite, then there must be at least one supported
+	 * hash-and-sign with the corresponding signature type.
+	 */
+	internal void CheckConfigHashAndSign()
+	{
+		/*
+		 * The hash-and-sign are only for TLS 1.2.
+		 */
+		if (VersionMax < SSL.TLS12) {
+			return;
+		}
+
+		/*
+		 * If the list is empty then we will work over the default
+		 * list inferred by the peer (no extension sent).
+		 */
+		if (SupportedHashAndSign == null
+			|| SupportedHashAndSign.Length == 0)
+		{
+			return;
+		}
+
+		bool needRSA = false;
+		bool needECDSA = false;
+		foreach (int cs in SupportedCipherSuites) {
+			if (SSL.IsECDHE_RSA(cs)) {
+				needRSA = true;
+			}
+			if (SSL.IsECDHE_ECDSA(cs)) {
+				needECDSA = true;
+			}
+		}
+		foreach (int hs in SupportedHashAndSign) {
+			int sa = hs & 0xFF;
+			if (needRSA && sa == SSL.RSA) {
+				needRSA = false;
+			}
+			if (needECDSA && sa == SSL.ECDSA) {
+				needECDSA = false;
+			}
+		}
+		if (needRSA) {
+			throw new SSLException("Incoherent configuration:"
+				+ " supports ECDHE_RSA but no RSA signature"
+				+ " (for TLS 1.2)");
+		}
+		if (needECDSA) {
+			throw new SSLException("Incoherent configuration:"
+				+ " supports ECDHE_ECDSA but no ECDSA signature"
+				+ " (for TLS 1.2)");
+		}
+	}
+
+	internal bool HasQuirk(string name)
+	{
+		return Quirks != null && Quirks.GetString(name, null) != null;
+	}
+
+	internal bool GetQuirkBool(string name)
+	{
+		return GetQuirkBool(name, false);
+	}
+
+	internal bool GetQuirkBool(string name, bool defaultValue)
+	{
+		if (Quirks == null) {
+			return false;
+		}
+		return Quirks.GetBoolean(name, defaultValue);
+	}
+
+	internal int GetQuirkInt(string name)
+	{
+		return GetQuirkInt(name, 0);
+	}
+
+	internal int GetQuirkInt(string name, int defaultValue)
+	{
+		if (Quirks == null) {
+			return defaultValue;
+		}
+		return Quirks.GetInteger(name, defaultValue);
+	}
+
+	internal string GetQuirkString(string name)
+	{
+		return GetQuirkString(name, null);
+	}
+
+	internal string GetQuirkString(string name, string defaultValue)
+	{
+		if (Quirks == null) {
+			return defaultValue;
+		}
+		return Quirks.GetString(name, defaultValue);
+	}
+
+	/*
+	 * Test whether the first handshake has been done or not.
+	 */
+	internal bool FirstHandshakeDone {
+		get {
+			return savedClientFinished != null;
+		}
+	}
+
+	/*
+	 * Close the engine. No I/O may happen beyond this call.
+	 */
+	internal void MarkFailed()
+	{
+		if (sub != null) {
+			try {
+				if (OnClose != null) {
+					OnClose(sub);
+				} else if (CloseSub) {
+					sub.Close();
+				}
+			} catch {
+				// ignored
+			}
+			sub = null;
+		}
+		state = STATE_CLOSED;
+	}
+
+	/*
+	 * Check that the current state is not closed.
+	 */
+	void CheckNotClosed()
+	{
+		if (state == STATE_CLOSED) {
+			throw new SSLException("Connection is closed");
+		}
+	}
+
+	/*
+	 * Check that we are ready to exchange application data. A
+	 * handshake is performed if necessary.
+	 */
+	void CheckAppData()
+	{
+		CheckNotClosed();
+		if (!FirstHandshakeDone) {
+			DoHandshakeWrapper();
+		} else if (state != STATE_APPDATA) {
+			throw new SSLException(
+				"Connection not ready for application data");
+		}
+	}
+
+	/*
+	 * Set the version for outgoing records.
+	 */
+	internal void SetOutputRecordVersion(int version)
+	{
+		outRec.SetVersion(version);
+	}
+
+	/*
+	 * Set the expected version for incoming records. This should be
+	 * used by the server code just after parsing the ClientHello,
+	 * because the client is supposed to send records matching the
+	 * protocol version decided by the server and sent in the
+	 * ServerHello.
+	 *
+	 * For a SSL client, this call in unnecessary because default
+	 * behaviour is to look at the version of the first incoming
+	 * record (containing the ServerHello for the server) and expect
+	 * all subsequent records to have the same version.
+	 */
+	internal void SetInputRecordVersion(int version)
+	{
+		inRec.SetExpectedVersion(version);
+	}
+
+	/*
+	 * Flush the underlying record engine.
+	 */
+	internal void FlushSub()
+	{
+		outRec.Flush();
+	}
+
+	/*
+	 * Get next record. This returns false only if the connection
+	 * turned out to be ended "properly".
+	 *
+	 * In all other cases, a record is obtained, and true is
+	 * returned. It is possible that the record contains no unread
+	 * data (it could be an empty record, or it could be an alert
+	 * record whose contents are automatically processed).
+	 */
+	internal bool NextRecord()
+	{
+		if (!inRec.NextRecord()) {
+			if (NoCloseNotify && (state == STATE_APPDATA
+				|| state == STATE_CLOSING))
+			{
+				/*
+				 * No close_notify, but we have been set
+				 * to tolerate it.
+				 */
+				ClosedWithoutNotify = true;
+				MarkFailed();
+				return false;
+			}
+			throw new SSLException("Unexpected transport closure");
+		}
+
+		/*
+		 * We basically ignore empty records, regardless of state.
+		 */
+		if (inRec.BufferedLength == 0) {
+			return true;
+		}
+
+		int rt = inRec.RecordType;
+		switch (rt) {
+
+		case SSL.ALERT:
+			/*
+			 * Fatal alerts trigger an exception. Warnings are
+			 * ignored, except close_notify and no_renegotiation.
+			 */
+			while (inRec.BufferedLength > 0) {
+				int level = deferredAlert;
+				deferredAlert = -1;
+				if (level < 0) {
+					level = inRec.Read();
+					if (inRec.BufferedLength == 0) {
+						deferredAlert = level;
+						break;
+					}
+				}
+				int desc = inRec.Read();
+				if (level == SSL.FATAL) {
+					throw new SSLException(desc);
+				}
+				if (level != SSL.WARNING) {
+					throw new SSLException("Unknown"
+						+ " alert level: " + level);
+				}
+				if (desc == SSL.CLOSE_NOTIFY) {
+					if (state == STATE_CLOSING) {
+						MarkFailed();
+						return false;
+					}
+					if (state != STATE_APPDATA) {
+						throw new SSLException(
+							"Unexpected closure");
+					}
+					Close(false);
+					return false;
+				} else if (desc == SSL.NO_RENEGOTIATION) {
+					receivedNoReneg = true;
+				}
+			}
+			return true;
+
+		case SSL.HANDSHAKE:
+			switch (state) {
+			case STATE_HANDSHAKE:
+				return true;
+			case STATE_APPDATA:
+				ProcessExtraHandshakeWrapper();
+				return true;
+			}
+			throw new SSLException("Unexpected handshake message");
+
+		case SSL.CHANGE_CIPHER_SPEC:
+			if (state == STATE_CCS) {
+				return true;
+			}
+			throw new SSLException("Unexpected Change Cipher Spec");
+
+		case SSL.APPLICATION_DATA:
+			if (state == STATE_APPDATA) {
+				return true;
+			}
+			throw new SSLException("Unexpected application data");
+
+		default:
+			throw new SSLException("Invalid record type: " + rt);
+
+		}
+	}
+
+	/*
+	 * ZRead() reads the next byte, possibly obtaining further records
+	 * to do so. It may return -1 only if the end-of-stream was reached,
+	 * which can happen only in "application data" state (after a
+	 * successful handshake).
+	 */
+	int ZRead()
+	{
+		int x = ZReadNoHash();
+		if (x >= 0 && inRec.RecordType == SSL.HANDSHAKE) {
+			HashExtra((byte)x);
+		}
+		return x;
+	}
+
+	/*
+	 * ZReadNoHash() is similar to ZRead() except that it skips
+	 * the automatic hashing of handshake messages.
+	 */
+	int ZReadNoHash()
+	{
+		while (inRec.BufferedLength == 0) {
+			if (!NextRecord()) {
+				return -1;
+			}
+		}
+		return inRec.Read();
+	}
+
+	/*
+	 * ZReadNoHashNoReneg() is similar to ZReadNoHash() except
+	 * that it may return -1 while being in state STATE_HANDSHAKE
+	 * in case a no_renegotiation alert is received.
+	 */
+	int ZReadNoHashNoReneg()
+	{
+		while (inRec.BufferedLength == 0) {
+			receivedNoReneg = false;
+			if (!NextRecord() || receivedNoReneg) {
+				return -1;
+			}
+		}
+		return inRec.Read();
+	}
+
+	/*
+	 * Read some bytes. At least one byte will be obtained, unless
+	 * EOF is reached. Extra records are obtained if necessary.
+	 */
+	int ZRead(byte[] buf)
+	{
+		return ZRead(buf, 0, buf.Length);
+	}
+
+	/*
+	 * Read some bytes. At least one byte will be obtained, unless
+	 * EOF is reached. Extra records are obtained if necessary.
+	 */
+	int ZRead(byte[] buf, int off, int len)
+	{
+		while (inRec.BufferedLength == 0) {
+			if (!NextRecord()) {
+				return 0;
+			}
+		}
+		int rlen = inRec.Read(buf, off, len);
+		if (rlen > 0 && inRec.RecordType == SSL.HANDSHAKE) {
+			md5.Update(buf, off, rlen);
+			sha1.Update(buf, off, rlen);
+			sha256.Update(buf, off, rlen);
+			sha384.Update(buf, off, rlen);
+		}
+		return rlen;
+	}
+
+	bool DoHandshakeWrapper()
+	{
+		/*
+		 * Record split mode syntax:  name:[types]
+		 *
+		 * 'name' is a symbolic name.
+		 *
+		 * 'types' is a comma-separated list of record types on
+		 *  which the splitting mode applies. Record types are
+		 *  numeric values (in decimal).
+		 */
+		string splitMode = GetQuirkString("recordSplitMode");
+		if (splitMode != null) {
+			splitMode = splitMode.Trim();
+			int j = splitMode.IndexOf(':');
+			int m = 0;
+			if (j >= 0) {
+				string w = splitMode.Substring(j + 1);
+				foreach (string s in w.Split(',')) {
+					m |= 1 << Int32.Parse(s.Trim());
+				}
+				splitMode = splitMode.Substring(0, j).Trim();
+			}
+			switch (splitMode.ToLowerInvariant()) {
+			case "half":
+				m |= OutputRecord.MODE_SPLIT_HALF;
+				break;
+			case "zero_before":
+				m |= OutputRecord.MODE_SPLIT_ZERO_BEFORE;
+				break;
+			case "zero_half":
+				m |= OutputRecord.MODE_SPLIT_ZERO_HALF;
+				break;
+			case "one_start":
+				m |= OutputRecord.MODE_SPLIT_ONE_START;
+				break;
+			case "one_end":
+				m |= OutputRecord.MODE_SPLIT_ONE_END;
+				break;
+			case "multi_one":
+				m |= OutputRecord.MODE_SPLIT_MULTI_ONE;
+				break;
+			default:
+				throw new SSLException(string.Format(
+					"Bad recordSplitMode name: '{0}'",
+					splitMode));
+			}
+			outRec.SetSplitMode(m);
+		}
+
+		/*
+		 * Triggers for extra empty records.
+		 */
+		outRec.SetThresholdZeroHandshake(
+			GetQuirkInt("thresholdZeroHandshake"));
+		outRec.SetThresholdZeroAppData(
+			GetQuirkInt("thresholdZeroAppData"));
+
+		try {
+			for (;;) {
+				bool ret = DoHandshake();
+				if (!ret) {
+					return false;
+				}
+				/*
+				 * There could be some extra handshake
+				 * data lingering in the input buffer, in
+				 * which case we must process it right away.
+				 */
+				if (HasBufferedHandshake) {
+					ProcessExtraHandshakeWrapper();
+				}
+				return true;
+			}
+		} catch {
+			MarkFailed();
+			throw;
+		}
+	}
+
+	void ProcessExtraHandshakeWrapper()
+	{
+		try {
+			while (HasBufferedHandshake) {
+				ProcessExtraHandshake();
+			}
+		} catch {
+			MarkFailed();
+			throw;
+		}
+	}
+
+	/*
+	 * Set the state to the provided value.
+	 */
+	internal void SetState(int state)
+	{
+		this.state = state;
+	}
+
+	/*
+	 * Reset running hashes for handshake messages.
+	 */
+	internal void ResetHashes()
+	{
+		md5.Reset();
+		sha1.Reset();
+		sha256.Reset();
+		sha384.Reset();
+	}
+
+	/*
+	 * Inject a specific byte value in the hash functions for handshake
+	 * messages.
+	 */
+	void HashExtra(byte b)
+	{
+		md5.Update(b);
+		sha1.Update(b);
+		sha256.Update(b);
+		sha384.Update(b);
+	}
+
+	/*
+	 * Run a handshake. This function normally returns true; it returns
+	 * false only if the call was a renegotiation attempt AND it was
+	 * denied by the peer.
+	 */
+	internal abstract bool DoHandshake();
+
+	/*
+	 * A non-empty handshake record has been received while we
+	 * were in post-handshake "application data" state. This
+	 * method should handle that message with the necessary
+	 * actions; if it returns false then the caller will fail
+	 * the connection with an exception.
+	 */
+	internal abstract void ProcessExtraHandshake();
+
+	/*
+	 * Perform the preparatory steps for a renegotiation. For a client,
+	 * there are no such steps, so this call is a no-op. For a server,
+	 * an HelloRequest should be sent.
+	 */
+	internal abstract void PrepareRenegotiate();
+
+	/*
+	 * Read the next handshake message. This also sets the state
+	 * to STATE_HANDSHAKE.
+	 */
+	internal byte[] ReadHandshakeMessage(out int msgType)
+	{
+		return ReadHandshakeMessage(out msgType, false);
+	}
+
+	/*
+	 * Read the next handshake message. This also sets the state
+	 * to STATE_HANDSHAKE. If tolerateNoReneg is true, then a
+	 * received no_renegotiation alert interrupts the reading, in
+	 * which case this function sets the state back to its previous
+	 * value and returns null.
+	 */
+	internal byte[] ReadHandshakeMessage(
+		out int msgType, bool tolerateNoReneg)
+	{
+		int oldState = state;
+		state = STATE_HANDSHAKE;
+
+		/*
+		 * In STATE_HANDSHAKE, an unexpected closure is never
+		 * tolerated, so ZRead() won't return -1.
+		 */
+		for (;;) {
+			msgType = ZReadNoHashNoReneg();
+			if (msgType < 0) {
+				if (tolerateNoReneg) {
+					state = oldState;
+					return null;
+				}
+				continue;
+			}
+			if (msgType == SSL.HELLO_REQUEST && IsClient) {
+				/*
+				 * Extra HelloRequest messages are ignored
+				 * (as long as they are properly empty), and
+				 * they don't contribute to the running hashes.
+				 */
+				if (ZReadNoHash() != 0
+					|| ZReadNoHash() != 0
+					|| ZReadNoHash() != 0)
+				{
+					throw new SSLException(
+						"Non-empty HelloRequest");
+				}
+				continue;
+			}
+			HashExtra((byte)msgType);
+			int len = ZRead();
+			len = (len << 8) + ZRead();
+			len = (len << 8) + ZRead();
+			if (len > MaximumHandshakeMessageLength) {
+				throw new SSLException(
+					"Oversized handshake message: len="
+					+ len);
+			}
+			byte[] buf = new byte[len];
+			int off = 0;
+			while (off < len) {
+				off += ZRead(buf, off, len - off);
+			}
+			return buf;
+		}
+	}
+
+	/*
+	 * Read the next handshake message; fail (with an exception) if
+	 * it does not have the specified type. This also sets the state
+	 * to STATE_HANDSHAKE.
+	 */
+	internal byte[] ReadHandshakeMessageExpected(int msgType)
+	{
+		int rmt;
+		byte[] msg = ReadHandshakeMessage(out rmt);
+		if (rmt != msgType) {
+			throw new SSLException(string.Format("Unexpected"
+				+ " handshake message {0} (expected: {1})",
+				rmt, msgType));
+		}
+		return msg;
+	}
+
+	/*
+	 * Read an HelloRequest message. If, after reading an HelloRequest,
+	 * the record is not empty, then other HelloRequest messages are
+	 * read.
+	 *
+	 * This method shall be called only when a non-empty record of type
+	 * handshake is buffered. It switches the state to STATE_HANDSHAKE.
+	 */
+	internal void ReadHelloRequests()
+	{
+		state = STATE_HANDSHAKE;
+		while (inRec.BufferedLength > 0) {
+			int x = ZReadNoHash();
+			if (x != SSL.HELLO_REQUEST) {
+				throw new SSLException(
+					"Unexpected handshake message");
+			}
+			if (ZReadNoHash() != 0x00
+				|| ZReadNoHash() != 0x00
+				|| ZReadNoHash() != 0x00)
+			{
+				throw new SSLException(
+					"Non-empty HelloRequest");
+			}
+		}
+	}
+
+	/*
+	 * Test whether there is some buffered handshake data.
+	 */
+	internal bool HasBufferedHandshake {
+		get {
+			return inRec.RecordType == SSL.HANDSHAKE
+				&& inRec.BufferedLength > 0;
+		}
+	}
+
+	static DateTime EPOCH =
+		new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
+
+	/*
+	 * Create a new client or server random.
+	 */
+	internal void MakeRandom(byte[] dst)
+	{
+		uint utc = (uint)((DateTime.UtcNow - EPOCH).Ticks / 10000000);
+		IO.Enc32be(utc, dst, 0);
+		RNG.GetBytes(dst, 4, dst.Length - 4);
+	}
+
+	/*
+	 * Create a MemoryStream with preloaded 4-byte handshake message
+	 * header.
+	 */
+	internal MemoryStream StartHandshakeMessage(int type)
+	{
+		MemoryStream ms = new MemoryStream();
+		ms.WriteByte((byte)type);
+		IO.Write24(ms, 0);
+		return ms;
+	}
+
+	/*
+	 * Finalise a handshake message, and send it.
+	 */
+	internal void EndHandshakeMessage(MemoryStream ms)
+	{
+		byte[] buf = ms.ToArray();
+		IO.Enc24be(buf.Length - 4, buf, 1);
+		outRec.RecordType = SSL.HANDSHAKE;
+		outRec.Write(buf);
+		md5.Update(buf);
+		sha1.Update(buf);
+		sha256.Update(buf);
+		sha384.Update(buf);
+	}
+
+	/*
+	 * Get the PRF corresponding to the negotiated protocol version
+	 * and cipher suite.
+	 */
+	internal PRF GetPRF()
+	{
+		if (Version <= SSL.TLS11) {
+			return new PRF();
+		} else {
+			return SSL.GetPRFForTLS12(CipherSuite);
+		}
+	}
+
+	/*
+	 * Compute the master secret from the provided premaster secret.
+	 */
+	internal void ComputeMaster(byte[] pms)
+	{
+		PRF prf = GetPRF();
+		byte[] seed = new byte[64];
+		Array.Copy(clientRandom, 0, seed, 0, 32);
+		Array.Copy(serverRandom, 0, seed, 32, 32);
+		prf.GetBytes(pms, PRF.LABEL_MASTER_SECRET, seed, masterSecret);
+	}
+
+	/*
+	 * Set the master secret to the provided value. This is used when
+	 * resuming a session.
+	 */
+	internal void SetMasterSecret(byte[] rms)
+	{
+		Array.Copy(rms, 0, masterSecret, 0, rms.Length);
+	}
+
+	/*
+	 * Switch to new security parameters.
+	 * 'write' is true if we switch encryption for our sending channel,
+	 * false for our receiving channel.
+	 */
+	internal void SwitchEncryption(bool write)
+	{
+		int macLen, encLen, ivLen;
+		IBlockCipher block = null;
+		IDigest hash = null;
+		Poly1305 pp = null;
+		switch (CipherSuite) {
+		case SSL.RSA_WITH_3DES_EDE_CBC_SHA:
+		case SSL.DH_DSS_WITH_3DES_EDE_CBC_SHA:
+		case SSL.DH_RSA_WITH_3DES_EDE_CBC_SHA:
+		case SSL.DHE_DSS_WITH_3DES_EDE_CBC_SHA:
+		case SSL.DHE_RSA_WITH_3DES_EDE_CBC_SHA:
+		case SSL.DH_anon_WITH_3DES_EDE_CBC_SHA:
+		case SSL.ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA:
+		case SSL.ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA:
+		case SSL.ECDH_RSA_WITH_3DES_EDE_CBC_SHA:
+		case SSL.ECDHE_RSA_WITH_3DES_EDE_CBC_SHA:
+		case SSL.ECDH_anon_WITH_3DES_EDE_CBC_SHA:
+			macLen = 20;
+			encLen = 24;
+			ivLen = 8;
+			block = new DES();
+			hash = new SHA1();
+			break;
+
+		case SSL.RSA_WITH_AES_128_CBC_SHA:
+		case SSL.DH_DSS_WITH_AES_128_CBC_SHA:
+		case SSL.DH_RSA_WITH_AES_128_CBC_SHA:
+		case SSL.DHE_DSS_WITH_AES_128_CBC_SHA:
+		case SSL.DHE_RSA_WITH_AES_128_CBC_SHA:
+		case SSL.DH_anon_WITH_AES_128_CBC_SHA:
+		case SSL.ECDH_ECDSA_WITH_AES_128_CBC_SHA:
+		case SSL.ECDHE_ECDSA_WITH_AES_128_CBC_SHA:
+		case SSL.ECDH_RSA_WITH_AES_128_CBC_SHA:
+		case SSL.ECDHE_RSA_WITH_AES_128_CBC_SHA:
+		case SSL.ECDH_anon_WITH_AES_128_CBC_SHA:
+			macLen = 20;
+			encLen = 16;
+			ivLen = 16;
+			block = new AES();
+			hash = new SHA1();
+			break;
+
+		case SSL.RSA_WITH_AES_256_CBC_SHA:
+		case SSL.DH_DSS_WITH_AES_256_CBC_SHA:
+		case SSL.DH_RSA_WITH_AES_256_CBC_SHA:
+		case SSL.DHE_DSS_WITH_AES_256_CBC_SHA:
+		case SSL.DHE_RSA_WITH_AES_256_CBC_SHA:
+		case SSL.DH_anon_WITH_AES_256_CBC_SHA:
+		case SSL.ECDH_ECDSA_WITH_AES_256_CBC_SHA:
+		case SSL.ECDHE_ECDSA_WITH_AES_256_CBC_SHA:
+		case SSL.ECDH_RSA_WITH_AES_256_CBC_SHA:
+		case SSL.ECDHE_RSA_WITH_AES_256_CBC_SHA:
+		case SSL.ECDH_anon_WITH_AES_256_CBC_SHA:
+			macLen = 20;
+			encLen = 32;
+			ivLen = 16;
+			block = new AES();
+			hash = new SHA1();
+			break;
+
+		case SSL.RSA_WITH_AES_128_CBC_SHA256:
+		case SSL.DH_DSS_WITH_AES_128_CBC_SHA256:
+		case SSL.DH_RSA_WITH_AES_128_CBC_SHA256:
+		case SSL.DHE_DSS_WITH_AES_128_CBC_SHA256:
+		case SSL.DHE_RSA_WITH_AES_128_CBC_SHA256:
+		case SSL.DH_anon_WITH_AES_128_CBC_SHA256:
+		case SSL.ECDHE_ECDSA_WITH_AES_128_CBC_SHA256:
+		case SSL.ECDH_ECDSA_WITH_AES_128_CBC_SHA256:
+		case SSL.ECDHE_RSA_WITH_AES_128_CBC_SHA256:
+		case SSL.ECDH_RSA_WITH_AES_128_CBC_SHA256:
+			macLen = 32;
+			encLen = 16;
+			ivLen = 16;
+			block = new AES();
+			hash = new SHA256();
+			break;
+
+		case SSL.RSA_WITH_AES_256_CBC_SHA256:
+		case SSL.DH_DSS_WITH_AES_256_CBC_SHA256:
+		case SSL.DH_RSA_WITH_AES_256_CBC_SHA256:
+		case SSL.DHE_DSS_WITH_AES_256_CBC_SHA256:
+		case SSL.DHE_RSA_WITH_AES_256_CBC_SHA256:
+		case SSL.DH_anon_WITH_AES_256_CBC_SHA256:
+			macLen = 32;
+			encLen = 32;
+			ivLen = 16;
+			block = new AES();
+			hash = new SHA256();
+			break;
+
+		case SSL.ECDHE_ECDSA_WITH_AES_256_CBC_SHA384:
+		case SSL.ECDH_ECDSA_WITH_AES_256_CBC_SHA384:
+		case SSL.ECDHE_RSA_WITH_AES_256_CBC_SHA384:
+		case SSL.ECDH_RSA_WITH_AES_256_CBC_SHA384:
+			macLen = 48;
+			encLen = 32;
+			ivLen = 16;
+			block = new AES();
+			hash = new SHA384();
+			break;
+
+		case SSL.RSA_WITH_AES_128_GCM_SHA256:
+		case SSL.DHE_RSA_WITH_AES_128_GCM_SHA256:
+		case SSL.DH_RSA_WITH_AES_128_GCM_SHA256:
+		case SSL.DHE_DSS_WITH_AES_128_GCM_SHA256:
+		case SSL.DH_DSS_WITH_AES_128_GCM_SHA256:
+		case SSL.DH_anon_WITH_AES_128_GCM_SHA256:
+		case SSL.ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
+		case SSL.ECDH_ECDSA_WITH_AES_128_GCM_SHA256:
+		case SSL.ECDHE_RSA_WITH_AES_128_GCM_SHA256:
+		case SSL.ECDH_RSA_WITH_AES_128_GCM_SHA256:
+			macLen = 0;
+			encLen = 16;
+			ivLen = 4;
+			block = new AES();
+			break;
+
+		case SSL.RSA_WITH_AES_256_GCM_SHA384:
+		case SSL.DHE_RSA_WITH_AES_256_GCM_SHA384:
+		case SSL.DH_RSA_WITH_AES_256_GCM_SHA384:
+		case SSL.DHE_DSS_WITH_AES_256_GCM_SHA384:
+		case SSL.DH_DSS_WITH_AES_256_GCM_SHA384:
+		case SSL.DH_anon_WITH_AES_256_GCM_SHA384:
+		case SSL.ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:
+		case SSL.ECDH_ECDSA_WITH_AES_256_GCM_SHA384:
+		case SSL.ECDHE_RSA_WITH_AES_256_GCM_SHA384:
+		case SSL.ECDH_RSA_WITH_AES_256_GCM_SHA384:
+			macLen = 0;
+			encLen = 32;
+			ivLen = 4;
+			block = new AES();
+			break;
+
+		case SSL.ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256:
+		case SSL.ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256:
+		case SSL.DHE_RSA_WITH_CHACHA20_POLY1305_SHA256:
+			macLen = 0;
+			encLen = 32;
+			ivLen = 12;
+			pp = new Poly1305();
+			pp.ChaCha = new ChaCha20();
+			break;
+
+		default:
+			throw new SSLException("Unsupported cipher suite");
+		}
+
+		/*
+		 * Normally we don't need IV when using CBC+HMAC with
+		 * TLS 1.1+.
+		 */
+		if (Version >= SSL.TLS11 && hash != null) {
+			ivLen = 0;
+		}
+
+		byte[] seed = new byte[64];
+		Array.Copy(serverRandom, 0, seed, 0, 32);
+		Array.Copy(clientRandom, 0, seed, 32, 32);
+		byte[] kb = GetPRF().GetBytes(masterSecret,
+			PRF.LABEL_KEY_EXPANSION, seed,
+			(macLen + encLen + ivLen) << 1);
+
+		/*
+		 * Test whether we need the client write keys, or the
+		 * server write keys.
+		 */
+		bool clientWrite = (IsClient == write);
+		HMAC hm = null;
+		if (macLen > 0) {
+			hm = new HMAC(hash);
+			hm.SetKey(kb, clientWrite ? 0 : macLen, macLen);
+		}
+		if (block != null) {
+			block.SetKey(kb, (macLen << 1)
+				+ (clientWrite ? 0 : encLen), encLen);
+		} else if (pp != null) {
+			pp.ChaCha.SetKey(kb, (macLen << 1)
+				+ (clientWrite ? 0 : encLen), encLen);
+		}
+		byte[] iv = null;
+		if (ivLen > 0) {
+			iv = new byte[ivLen];
+			Array.Copy(kb, ((macLen + encLen) << 1)
+				+ (clientWrite ? 0 : ivLen), iv, 0, ivLen);
+		}
+
+		if (hm != null) {
+			/*
+			 * CBC+HMAC cipher suite.
+			 */
+			if (write) {
+				outRec.SetEncryption(
+					new RecordEncryptCBC(block, hm, iv));
+			} else {
+				inRec.SetDecryption(
+					new RecordDecryptCBC(block, hm, iv));
+			}
+		} else if (block != null) {
+			/*
+			 * GCM cipher suite.
+			 */
+			if (write) {
+				outRec.SetEncryption(
+					new RecordEncryptGCM(block, iv));
+			} else {
+				inRec.SetDecryption(
+					new RecordDecryptGCM(block, iv));
+			}
+		} else if (pp != null) {
+			/*
+			 * ChaCha20 + Poly1305 cipher suite.
+			 */
+			if (write) {
+				outRec.SetEncryption(
+					new RecordEncryptChaPol(pp, iv));
+			} else {
+				inRec.SetDecryption(
+					new RecordDecryptChaPol(pp, iv));
+			}
+		} else {
+			throw new Exception("NYI");
+		}
+	}
+
+	/*
+	 * Compute Finished message. The 'client' flag is set to true
+	 * for the Finished message sent by the client, false for the
+	 * Finished message sent by the server.
+	 */
+	internal byte[] ComputeFinished(bool client)
+	{
+		PRF prf;
+		byte[] seed;
+		if (Version <= SSL.TLS11) {
+			seed = new byte[36];
+			md5.DoPartial(seed, 0);
+			sha1.DoPartial(seed, 16);
+			prf = new PRF();
+		} else if (SSL.IsSHA384(CipherSuite)) {
+			seed = sha384.DoPartial();
+			prf = new PRF(new SHA384());
+		} else {
+			seed = sha256.DoPartial();
+			prf = new PRF(new SHA256());
+		}
+		byte[] label = client
+			? PRF.LABEL_CLIENT_FINISHED
+			: PRF.LABEL_SERVER_FINISHED;
+		return prf.GetBytes(masterSecret, label, seed, 12);
+	}
+
+	/*
+	 * Send a ChangeCipherSpec, then a Finished message. This
+	 * call implies switching to the new encryption parameters for
+	 * the sending channel.
+	 */
+	internal void SendCCSAndFinished()
+	{
+		outRec.RecordType = SSL.CHANGE_CIPHER_SPEC;
+		outRec.Write(0x01);
+		outRec.RecordType = SSL.HANDSHAKE;
+		SwitchEncryption(true);
+		byte[] fin = ComputeFinished(IsClient);
+		if (IsClient) {
+			savedClientFinished = fin;
+		} else {
+			savedServerFinished = fin;
+		}
+		MemoryStream ms = StartHandshakeMessage(SSL.FINISHED);
+		ms.Write(fin, 0, fin.Length);
+		EndHandshakeMessage(ms);
+	}
+
+	/*
+	 * Receive a ChangeCipherSpec, then a Finished message. This
+	 * call implies switching to the new encryption parameters for
+	 * the receiving channel.
+	 */
+	internal void ParseCCSAndFinished()
+	{
+		if (inRec.BufferedLength > 0) {
+			throw new SSLException(
+				"Buffered data while expecting CCS");
+		}
+		state = STATE_CCS;
+		int x = ZRead();
+		if (x != 0x01 || inRec.BufferedLength > 0) {
+			throw new SSLException("Invalid CCS contents");
+		}
+		SwitchEncryption(false);
+		state = STATE_HANDSHAKE;
+		byte[] fin = ComputeFinished(!IsClient);
+		byte[] msg = ReadHandshakeMessageExpected(SSL.FINISHED);
+		if (!Eq(fin, msg)) {
+			throw new SSLException("Wrong Finished value");
+		}
+		if (inRec.BufferedLength > 0) {
+			throw new SSLException(
+				"Extra handshake data after Finished message");
+		}
+		if (IsClient) {
+			savedServerFinished = fin;
+		} else {
+			savedClientFinished = fin;
+		}
+	}
+
+	/*
+	 * Switch to "application data" state just after the handshake.
+	 */
+	internal void SetAppData()
+	{
+		SetState(STATE_APPDATA);
+		outRec.RecordType = SSL.APPLICATION_DATA;
+	}
+
+	/*
+	 * Send an alert of level "warning".
+	 */
+	internal void SendWarning(int type)
+	{
+		int rt = outRec.RecordType;
+		outRec.RecordType = SSL.ALERT;
+		outRec.Write(SSL.WARNING);
+		outRec.Write((byte)type);
+		outRec.Flush();
+		outRec.RecordType = rt;
+	}
+
+	static bool Eq(byte[] a, byte[] b)
+	{
+		int n = a.Length;
+		if (n != b.Length) {
+			return false;
+		}
+		int z = 0;
+		for (int i = 0; i < n; i ++) {
+			z |= a[i] ^ b[i];
+		}
+		return z == 0;
+	}
+}
+
+}
diff --git a/SSLTLS/SSLException.cs b/SSLTLS/SSLException.cs
new file mode 100644
index 0000000..1c36564
--- /dev/null
+++ b/SSLTLS/SSLException.cs
@@ -0,0 +1,57 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.IO;
+
+namespace SSLTLS {
+
+/*
+ * All SSL failures (invalid messages, failed decryption or MAC checks,
+ * received fatal alerts...) trigger an SSLException. Since SSLEngine
+ * is a Stream, this exception is made an extension of IOException.
+ */
+
+public class SSLException : IOException {
+
+	public SSLException(string msg) : base(msg)
+	{
+	}
+
+	public SSLException(string msg, Exception cause) : base(msg, cause)
+	{
+	}
+
+	public SSLException(Exception cause) : base(cause.Message, cause)
+	{
+	}
+
+	public SSLException(int alert)
+		: base(String.Format(
+			"Fatal alert {0} received from peer", alert))
+	{
+	}
+}
+
+}
diff --git a/SSLTLS/SSLQuirks.cs b/SSLTLS/SSLQuirks.cs
new file mode 100644
index 0000000..2a025e3
--- /dev/null
+++ b/SSLTLS/SSLQuirks.cs
@@ -0,0 +1,197 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace SSLTLS {
+
+/*
+ * An SSLQuirks instance is a set of named parameters that can be
+ * applied to an SSL engine (client or server) and alter its behaviour.
+ * Some of these variants are rarely used options permitted by the
+ * standard; others are downright invalid, and are meant to verify that
+ * a peer implementation is properly reacting to malformed messages
+ * and other anomalous conditions.
+ *
+ * Parameter names and values are strings. Parameter names are
+ * case-sensitive. As a matter of convention:
+ *
+ *   - Boolean values are set as "true" or "false" string values.
+ *
+ *   - Integers are encoded in decimal or hexadecimal; hexadecimal
+ *     values have a leading "0x" header (or "-0x" for a negative
+ *     hexadecimal value).
+ */
+
+public class SSLQuirks {
+
+	/*
+	 * When reading a parameter value, and the parameter is not
+	 * defined, null is returned.
+	 */
+	public string this[string name] {
+		get {
+			string v;
+			if (d.TryGetValue(name, out v)) {
+				return v;
+			} else {
+				return null;
+			}
+		}
+		set {
+			d[name] = value;
+		}
+	}
+
+	IDictionary<string, string> d;
+
+	public SSLQuirks()
+	{
+		d = new SortedDictionary<string, string>(
+			StringComparer.Ordinal);
+	}
+
+	/*
+	 * Get a boolean quirk. If defined, then the boolean value is
+	 * written in 'val' and true is returned; otherwise, 'val' is
+	 * set to false, and false is returned.
+	 */
+	public bool TryGetBoolean(string name, out bool val)
+	{
+		string s;
+		if (!d.TryGetValue(name, out s)) {
+			val = false;
+			return false;
+		}
+		switch (s) {
+		case "true":   val = true; return true;
+		case "false":  val = false; return true;
+		}
+		s = s.ToLowerInvariant();
+		switch (s) {
+		case "true":   val = true; return true;
+		case "false":  val = false; return true;
+		}
+		throw new Exception("Quirk value is not a boolean");
+	}
+
+	/*
+	 * Get a boolean quirk. If undefined, the provided default value
+	 * is returned.
+	 */
+	public bool GetBoolean(string name, bool defaultValue)
+	{
+		bool val;
+		if (TryGetBoolean(name, out val)) {
+			return val;
+		} else {
+			return defaultValue;
+		}
+	}
+
+	/*
+	 * Get an integer quirk. If defined, then the integer value is
+	 * written in 'val' and true is returned; otherwise, 'val' is
+	 * set to 0, and false is returned.
+	 */
+	public bool TryGetInteger(string name, out int val)
+	{
+		string s;
+		if (!d.TryGetValue(name, out s)) {
+			val = 0;
+			return false;
+		}
+		bool neg = false;
+		if (s.StartsWith("-")) {
+			neg = true;
+			s = s.Substring(1);
+		}
+		int radix;
+		if (s.StartsWith("0x")) {
+			radix = 16;
+			s = s.Substring(2);
+		} else {
+			radix = 10;
+		}
+		int acc = 0;
+		if (s.Length == 0) {
+			throw new Exception("Quirk value is not an integer");
+		}
+		foreach (char c in s) {
+			int x;
+			if (c >= '0' && c <= '9') {
+				x = c - '0';
+			} else if (c >= 'A' && c <= 'F') {
+				x = c - ('A' - 10);
+			} else if (c >= 'a' && c <= 'f') {
+				x = c - ('a' - 10);
+			} else {
+				throw new Exception(
+					"Quirk value is not an integer");
+			}
+			if (x >= radix) {
+				throw new Exception(
+					"Quirk value is not an integer");
+			}
+			acc = (acc * radix) + x;
+		}
+		if (neg) {
+			acc = -acc;
+		}
+		val = acc;
+		return true;
+	}
+
+	/*
+	 * Get an integer quirk. If undefined, the provided default value
+	 * is returned.
+	 */
+	public int GetInteger(string name, int defaultValue)
+	{
+		int val;
+		if (TryGetInteger(name, out val)) {
+			return val;
+		} else {
+			return defaultValue;
+		}
+	}
+
+	/*
+	 * Get a string quirk. If undefined, the provided default value
+	 * is returned.
+	 */
+	public string GetString(string name, string defaultValue)
+	{
+		string s;
+		if (d.TryGetValue(name, out s)) {
+			return s;
+		} else {
+			return defaultValue;
+		}
+	}
+}
+
+}
diff --git a/SSLTLS/SSLServer.cs b/SSLTLS/SSLServer.cs
new file mode 100644
index 0000000..6f39135
--- /dev/null
+++ b/SSLTLS/SSLServer.cs
@@ -0,0 +1,1019 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+using Crypto;
+
+namespace SSLTLS {
+
+/*
+ * Class for a SSL server connection.
+ *
+ * An instance is created over a specified transport stream. An optional
+ * cache for session parameters can be provided, to support session
+ * resumption. The instance handles the connection but cannot be
+ * "revived" after the connection was closed (the session parameters,
+ * though, can be extracted and used with another instance).
+ */
+
+public class SSLServer : SSLEngine {
+
+	/*
+	 * If true, then the server will enforce its own preference
+	 * order for cipher suite selection; otherwise, it will follow
+	 * the client's preferences. Default value is false.
+	 */
+	public bool EnforceServerOrder {
+		get; set;
+	}
+
+	/*
+	 * Server policy object, that selects cipher suite and certificate
+	 * chain to send to client. Such a policy object MUST be set
+	 * before the initial handshake takes place. This property is
+	 * initialised to the value provided as second argument to the
+	 * SSLServer constructor.
+	 */
+	public IServerPolicy ServerPolicy {
+		get; set;
+	}
+
+	/*
+	 * Optional session cache for SSL sessions. If null, then no
+	 * cache is used. Default value is null.
+	 */
+	public ISessionCache SessionCache {
+		get; set;
+	}
+
+	/*
+	 * If this flag is set to true, then session resumption will be
+	 * rejected; all handshakes will be full handshakes. Main
+	 * intended usage is when a server wants to renegotiate and ask
+	 * for a client certificate. Note that even if that flag is set,
+	 * each session resulting from a full handshake is still pushed
+	 * to the session cache (if configured in SessionCache).
+	 * Default value is false, meaning that session resumption is
+	 * allowed (but won't happen anyway if no session cache was
+	 * set in SessionCache).
+	 */
+	public bool NoResume {
+		get; set;
+	}
+
+	/*
+	 * Get the maximum supported version announced by the client
+	 * in its ClientHello message.
+	 */
+	public int ClientVersionMax {
+		get; private set;
+	}
+
+	/*
+	 * Get the list of hash and signature algorithms supported by the
+	 * client. Each value is a 16-bit integer, with the high byte
+	 * being the hash algorithm, and the low byte the signature
+	 * algorithm.
+	 *
+	 * The list is trimmed to include only hash and signature algorithms
+	 * that are supported by both client and server. It is ordered
+	 * by client or server preference, depending on the value of
+	 * the EnforceServerOrder flag.
+	 *
+	 * If the client did not send the dedicated extension, then the
+	 * list is inferred from the sent cipher suite, as specified
+	 * by RFC 5246, section 7.4.1.4.1.
+	 */
+	public List<int> ClientHashAndSign {
+		get; internal set;
+	}
+
+	/*
+	 * Get the list of elliptic curves supported by the client. Each
+	 * entry is a 16-bit integer that identifies a named curve. The
+	 * list is ordered by client preferences.
+	 *
+	 * If the client did not send the Supported Curves extension,
+	 * then the list will be inferred to contain NIST P-256 only
+	 * (if the client supports at least one ECDH, ECDHE or ECDSA
+	 * cipher suite), or to be empty (if the client does not support
+	 * any EC-based cipher suite).
+	 */
+	public List<int> ClientCurves {
+		get; internal set;
+	}
+
+	/*
+	 * Get the list of elliptic curves supported by both client and
+	 * server. Each entry is a 16-bit integer that identifies a
+	 * named curve. The list is ordered by preference (client or
+	 * server, depending on configuration). This list is the one
+	 * used for curve selection for ECDHE.
+	 */
+	public List<int> CommonCurves {
+		get; internal set;
+	}
+
+	/*
+	 * Get the list of cipher suites supported by the client. The
+	 * order matches the configured preferences (client or server
+	 * preference order, depending on the EnforceServerOrder flag).
+	 * Moreover, the list is trimmed:
+	 *
+	 *  - Signalling cipher suites ("SCSV") have been removed.
+	 *
+	 *  - Only suites supported by both client and server are kept.
+	 *
+	 *  - Suites that require TLS 1.2 are omitted if the selected
+	 *    protocol version is TLS 1.0 or 1.1.
+	 *
+	 *  - Suites that require client support for RSA signatures are
+	 *    removed if there is no common support for RSA signatures.
+	 *
+	 *  - Suites that require client support for ECDSA signatures
+	 *    are removed if there is no common support for ECDSA
+	 *    signatures.
+	 *
+	 *  - ECDHE suites are removed if there is no common support for
+	 *    elliptic curves.
+	 */
+	public List<int> CommonCipherSuites {
+		get; internal set;
+	}
+
+	IServerChoices serverChoices;
+	ECCurve ecdheCurve;
+	byte[] ecdheSecret;
+
+	/*
+	 * Create an SSL server instance, over the provided stream.
+	 * The 'serverPolicy' parameter is used as initial value to
+	 * the ServerPolicy property.
+	 */
+	public SSLServer(Stream sub, IServerPolicy serverPolicy)
+		: base(sub)
+	{
+		EnforceServerOrder = false;
+		ServerPolicy = serverPolicy;
+	}
+
+	internal override bool IsClient {
+		get {
+			return false;
+		}
+	}
+
+	internal override bool DoHandshake()
+	{
+		CheckConfigHashAndSign();
+
+		ResetHashes();
+		MakeRandom(serverRandom);
+
+		bool resume;
+		if (!ParseClientHello(out resume)) {
+			return false;
+		}
+		HandshakeCount ++;
+		SetOutputRecordVersion(Version);
+		SetInputRecordVersion(Version);
+
+		if (resume) {
+			SendServerHello();
+			SendCCSAndFinished();
+			FlushSub();
+			ParseCCSAndFinished();
+			SetAppData();
+			IsResume = true;
+			return true;
+		}
+
+		SendServerHello();
+		SendCertificate();
+		if (SSL.IsECDHE(CipherSuite)) {
+			SendServerKeyExchange();
+		}
+		SendServerHelloDone();
+		FlushSub();
+
+		ParseClientKeyExchange();
+		ParseCCSAndFinished();
+		SendCCSAndFinished();
+		FlushSub();
+		SetAppData();
+		IsResume = false;
+		if (SessionCache != null) {
+			SessionCache.Store(SessionParameters);
+		}
+		return true;
+	}
+
+	bool ParseClientHello(out bool resume)
+	{
+		resume = false;
+		int mt;
+		byte[] msg = ReadHandshakeMessage(out mt, FirstHandshakeDone);
+		if (msg == null) {
+			/*
+			 * Client rejected renegotiation attempt. This cannot
+			 * happen if we are invoked from
+			 * ProcessExtraHandshake() because that method is
+			 * invoked only when there is buffered handshake
+			 * data.
+			 */
+			return false;
+		}
+		if (mt != SSL.CLIENT_HELLO) {
+			throw new SSLException(string.Format("Unexpected"
+				+ " handshake message {0} (expecting a"
+				+ " ClientHello)", mt));
+		}
+
+		/*
+		 * Maximum protocol version supported by the client.
+		 */
+		if (msg.Length < 35) {
+			throw new SSLException("Invalid ClientHello");
+		}
+		ClientVersionMax = IO.Dec16be(msg, 0);
+		if (ClientVersionMax < VersionMin) {
+			throw new SSLException(string.Format(
+				"No acceptable version (client max = 0x{0:X4})",
+				ClientVersionMax));
+		}
+
+		/*
+		 * Client random (32 bytes).
+		 */
+		Array.Copy(msg, 2, clientRandom, 0, 32);
+
+		/*
+		 * Session ID sent by the client: at most 32 bytes.
+		 */
+		int idLen = msg[34];
+		int off = 35;
+		if (idLen > 32 || (off + idLen) > msg.Length) {
+			throw new SSLException("Invalid ClientHello");
+		}
+		byte[] clientSessionID = new byte[idLen];
+		Array.Copy(msg, off, clientSessionID, 0, idLen);
+		off += idLen;
+
+		/*
+		 * List of client cipher suites.
+		 */
+		if ((off + 2) > msg.Length) {
+			throw new SSLException("Invalid ClientHello");
+		}
+		int csLen = IO.Dec16be(msg, off);
+		off += 2;
+		if ((off + csLen) > msg.Length) {
+			throw new SSLException("Invalid ClientHello");
+		}
+		List<int> clientSuites = new List<int>();
+		bool seenReneg = false;
+		while (csLen > 0) {
+			int cs = IO.Dec16be(msg, off);
+			off += 2;
+			csLen -= 2;
+			if (cs == SSL.FALLBACK_SCSV) {
+				if (ClientVersionMax < VersionMax) {
+					throw new SSLException(
+						"Undue fallback detected");
+				}
+			} else if (cs == SSL.EMPTY_RENEGOTIATION_INFO_SCSV) {
+				if (FirstHandshakeDone) {
+					throw new SSLException(
+						"Reneg SCSV in renegotiation");
+				}
+				seenReneg = true;
+			} else {
+				clientSuites.Add(cs);
+			}
+		}
+
+		/*
+		 * List of compression methods. We only accept method 0
+		 * (no compression).
+		 */
+		if ((off + 1) > msg.Length) {
+			throw new SSLException("Invalid ClientHello");
+		}
+		int compLen = msg[off ++];
+		if ((off + compLen) > msg.Length) {
+			throw new SSLException("Invalid ClientHello");
+		}
+		bool foundUncompressed = false;
+		while (compLen -- > 0) {
+			if (msg[off ++] == 0x00) {
+				foundUncompressed = true;
+			}
+		}
+		if (!foundUncompressed) {
+			throw new SSLException("No common compression support");
+		}
+
+		/*
+		 * Extensions.
+		 */
+		ClientHashAndSign = null;
+		ClientCurves = null;
+		if (off < msg.Length) {
+			if ((off + 2) > msg.Length) {
+				throw new SSLException("Invalid ClientHello");
+			}
+			int tlen = IO.Dec16be(msg, off);
+			off += 2;
+			if ((off + tlen) != msg.Length) {
+				throw new SSLException("Invalid ClientHello");
+			}
+			while (off < msg.Length) {
+				if ((off + 4) > msg.Length) {
+					throw new SSLException(
+						"Invalid ClientHello");
+				}
+				int etype = IO.Dec16be(msg, off);
+				int elen = IO.Dec16be(msg, off + 2);
+				off += 4;
+				if ((off + elen) > msg.Length) {
+					throw new SSLException(
+						"Invalid ClientHello");
+				}
+				switch (etype) {
+
+				case 0x0000:
+					ParseExtSNI(msg, off, elen);
+					break;
+
+				case 0x000D:
+					ParseExtSignatures(msg, off, elen);
+					break;
+
+				case 0x000A:
+					ParseExtCurves(msg, off, elen);
+					break;
+
+				case 0xFF01:
+					ParseExtSecureReneg(msg, off, elen);
+					seenReneg = true;
+					break;
+
+				// Max Frag Length
+				// ALPN
+				// FIXME
+				}
+
+				off += elen;
+			}
+		}
+
+		/*
+		 * If we are renegotiating and we did not see the
+		 * Secure Renegotiation extension, then this is an error.
+		 */
+		if (FirstHandshakeDone && !seenReneg) {
+			throw new SSLException(
+				"Missing Secure Renegotiation extension");
+		}
+
+		/*
+		 * Use prescribed default values for supported algorithms
+		 * and curves, when not otherwise advertised by the client.
+		 */
+		if (ClientCurves == null) {
+			ClientCurves = new List<int>();
+			foreach (int cs in clientSuites) {
+				if (SSL.IsECDH(cs) || SSL.IsECDHE(cs)) {
+					ClientCurves.Add(SSL.NIST_P256);
+					break;
+				}
+			}
+		}
+		if (ClientHashAndSign == null) {
+			bool withRSA = false;
+			bool withECDSA = false;
+			foreach (int cs in clientSuites) {
+				if (SSL.IsRSA(cs)
+					|| SSL.IsECDH_RSA(cs)
+					|| SSL.IsECDHE_RSA(cs))
+				{
+					withRSA = true;
+				}
+				if (SSL.IsECDH_ECDSA(cs)
+					|| SSL.IsECDHE_ECDSA(cs))
+				{
+					withECDSA = true;
+				}
+			}
+			ClientHashAndSign = new List<int>();
+			if (withRSA) {
+				ClientHashAndSign.Add(SSL.RSA_SHA1);
+			}
+			if (withECDSA) {
+				ClientHashAndSign.Add(SSL.ECDSA_SHA1);
+			}
+		}
+
+		/*
+		 * Filter curves and algorithms with regards to our own
+		 * configuration.
+		 */
+		CommonCurves = FilterList(ClientCurves,
+			SupportedCurves, EnforceServerOrder);
+		ClientHashAndSign = FilterList(ClientHashAndSign,
+			SupportedHashAndSign, EnforceServerOrder);
+
+		/*
+		 * Selected protocol version (can be overridden by
+		 * resumption).
+		 */
+		Version = Math.Min(ClientVersionMax, VersionMax);
+
+		/*
+		 * Recompute list of acceptable cipher suites. We keep
+		 * only suites which are common to the client and server,
+		 * with some extra filters.
+		 *
+		 * Note that when using static ECDH, it is up to the
+		 * policy callback to determine whether the curves match
+		 * the contents of the certificate.
+		 *
+		 * We also build a list of common suites for session
+		 * resumption: this one may include suites whose
+		 * asymmetric crypto is not supported, because session
+		 * resumption uses only symmetric crypto.
+		 */
+		CommonCipherSuites = new List<int>();
+		List<int> commonSuitesResume = new List<int>();
+		bool canTLS12 = Version >= SSL.TLS12;
+		bool canSignRSA;
+		bool canSignECDSA;
+		if (Version >= SSL.TLS12) {
+			canSignRSA = false;
+			canSignECDSA = false;
+			foreach (int alg in ClientHashAndSign) {
+				int sa = alg & 0xFF;
+				switch (sa) {
+				case SSL.RSA:    canSignRSA = true;    break;
+				case SSL.ECDSA:  canSignECDSA = true;  break;
+				}
+			}
+		} else {
+			/*
+			 * For pre-1.2, the hash-and-sign configuration does
+			 * not matter, only the cipher suites themselves. So
+			 * we claim support of both RSA and ECDSA signatures
+			 * to avoid trimming the list too much.
+			 */
+			canSignRSA = true;
+			canSignECDSA = true;
+		}
+		bool canECDHE = CommonCurves.Count > 0;
+
+		foreach (int cs in clientSuites) {
+			if (!canTLS12 && SSL.IsTLS12(cs)) {
+				continue;
+			}
+			commonSuitesResume.Add(cs);
+			if (!canECDHE && SSL.IsECDHE(cs)) {
+				continue;
+			}
+			if (!canSignRSA && SSL.IsECDHE_RSA(cs)) {
+				continue;
+			}
+			if (!canSignECDSA && SSL.IsECDHE_ECDSA(cs)) {
+				continue;
+			}
+			CommonCipherSuites.Add(cs);
+		}
+		CommonCipherSuites = FilterList(CommonCipherSuites,
+			SupportedCipherSuites, EnforceServerOrder);
+		commonSuitesResume = FilterList(commonSuitesResume,
+			SupportedCipherSuites, EnforceServerOrder);
+
+		/*
+		 * If resuming, then use the remembered session parameters,
+		 * but only if they are compatible with what the client
+		 * sent AND what we currently support.
+		 */
+		SSLSessionParameters sp = null;
+		if (idLen > 0 && !NoResume && SessionCache != null) {
+			sp = SessionCache.Retrieve(
+				clientSessionID, ServerName);
+			if (sp != null && sp.ServerName != null
+				&& ServerName != null)
+			{
+				/*
+				 * When resuming a session, if there is
+				 * an explicit name sent by the client,
+				 * and the cached parameters also include
+				 * an explicit name, then both names
+				 * shall match.
+				 */
+				string s1 = sp.ServerName.ToLowerInvariant();
+				string s2 = ServerName.ToLowerInvariant();
+				if (s1 != s2) {
+					sp = null;
+				}
+			}
+		}
+		if (sp != null) {
+			bool resumeOK = true;
+			if (sp.Version < VersionMin
+				|| sp.Version > VersionMax
+				|| sp.Version > ClientVersionMax)
+			{
+				resumeOK = false;
+			}
+			if (!commonSuitesResume.Contains(sp.CipherSuite)) {
+				resumeOK = false;
+			}
+
+			if (resumeOK) {
+				/*
+				 * Session resumption is acceptable.
+				 */
+				resume = true;
+				sessionID = clientSessionID;
+				Version = sp.Version;
+				CipherSuite = sp.CipherSuite;
+				sessionID = clientSessionID;
+				SetMasterSecret(sp.MasterSecret);
+				return true;
+			}
+		}
+
+		/*
+		 * Not resuming. Let's select parameters.
+		 * Protocol version was already set.
+		 */
+		if (CommonCipherSuites.Count == 0) {
+			throw new SSLException("No common cipher suite");
+		}
+		serverChoices = ServerPolicy.Apply(this);
+		CipherSuite = serverChoices.GetCipherSuite();
+
+		/*
+		 * We create a new session ID, even if we don't have a
+		 * session cache, because the session parameters could
+		 * be extracted manually by the application.
+		 */
+		sessionID = new byte[32];
+		RNG.GetBytes(sessionID);
+
+		return true;
+	}
+
+	void ParseExtSNI(byte[] buf, int off, int len)
+	{
+		if (len < 2) {
+			throw new SSLException("Invalid SNI extension");
+		}
+		int tlen = IO.Dec16be(buf, off);
+		off += 2;
+		if ((tlen + 2) != len) {
+			throw new SSLException("Invalid SNI extension");
+		}
+		int lim = off + tlen;
+		bool found = false;
+		while (off < lim) {
+			if ((off + 3) > lim) {
+				throw new SSLException("Invalid SNI extension");
+			}
+			int ntype = buf[off ++];
+			int nlen = IO.Dec16be(buf, off);
+			off += 2;
+			if ((off + nlen) > lim) {
+				throw new SSLException("Invalid SNI extension");
+			}
+			if (ntype == 0) {
+				/*
+				 * Name type is "host name". There shall be
+				 * only one (at most) in the extension.
+				 */
+				if (found) {
+					throw new SSLException("Several host"
+						+ " names in SNI extension");
+				}
+				found = true;
+
+				/*
+				 * Verify that the name contains only
+				 * printable non-space ASCII, and normalise
+				 * it to lowercase.
+				 */
+				char[] tc = new char[nlen];
+				for (int i = 0; i < nlen; i ++) {
+					int x = buf[off + i];
+					if (x <= 32 || x >= 126) {
+						throw new SSLException(
+							"Invalid SNI hostname");
+					}
+					if (x >= 'A' && x <= 'Z') {
+						x += ('a' - 'A');
+					}
+					tc[i] = (char)x;
+				}
+				ServerName = new string(tc);
+			}
+			off += nlen;
+		}
+	}
+
+	void ParseExtSignatures(byte[] buf, int off, int len)
+	{
+		if (len < 2) {
+			throw new SSLException("Invalid signatures extension");
+		}
+		int tlen = IO.Dec16be(buf, off);
+		off += 2;
+		if (len != (tlen + 2)) {
+			throw new SSLException("Invalid signatures extension");
+		}
+		if ((tlen & 1) != 0) {
+			throw new SSLException("Invalid signatures extension");
+		}
+		ClientHashAndSign = new List<int>();
+		while (tlen > 0) {
+			ClientHashAndSign.Add(IO.Dec16be(buf, off));
+			off += 2;
+			tlen -= 2;
+		}
+	}
+
+	void ParseExtCurves(byte[] buf, int off, int len)
+	{
+		if (len < 2) {
+			throw new SSLException("Invalid curves extension");
+		}
+		int tlen = IO.Dec16be(buf, off);
+		off += 2;
+		if (len != (tlen + 2)) {
+			throw new SSLException("Invalid curves extension");
+		}
+		if ((tlen & 1) != 0) {
+			throw new SSLException("Invalid curves extension");
+		}
+		ClientCurves = new List<int>();
+		while (tlen > 0) {
+			ClientCurves.Add(IO.Dec16be(buf, off));
+			off += 2;
+			tlen -= 2;
+		}
+	}
+
+	void ParseExtSecureReneg(byte[] buf, int off, int len)
+	{
+		if (len < 1 || len != 1 + buf[off]) {
+			throw new SSLException(
+				"Invalid Secure Renegotiation extension");
+		}
+		len --;
+		off ++;
+
+		if (renegSupport == 0) {
+			/*
+			 * Initial handshake: extension MUST be empty.
+			 */
+			if (len != 0) {
+				throw new SSLException(
+					"Non-empty Secure Renegotation"
+					+ " on initial handshake");
+			}
+			renegSupport = 1;
+		} else {
+			/*
+			 * Renegotiation: extension MUST contain the
+			 * saved client Finished message.
+			 */
+			if (len != 12) {
+				throw new SSLException(
+					"Wrong Secure Renegotiation value");
+			}
+			int z = 0;
+			for (int i = 0; i < 12; i ++) {
+				z |= savedClientFinished[i] ^ buf[off + i];
+			}
+			if (z != 0) {
+				throw new SSLException(
+					"Wrong Secure Renegotiation value");
+			}
+		}
+	}
+
+	void SendServerHello()
+	{
+		MemoryStream ms = StartHandshakeMessage(SSL.SERVER_HELLO);
+
+		// Protocol version
+		IO.Write16(ms, Version);
+
+		// Server random
+		ms.Write(serverRandom, 0, serverRandom.Length);
+
+		// Session ID
+		ms.WriteByte((byte)sessionID.Length);
+		ms.Write(sessionID, 0, sessionID.Length);
+
+		// Cipher suite
+		IO.Write16(ms, CipherSuite);
+
+		// Compression
+		ms.WriteByte(0x00);
+
+		// Extensions
+		MemoryStream chExt = new MemoryStream();
+
+		// Secure renegotiation
+		if (!GetQuirkBool("noSecureReneg")) {
+			byte[] exv = null;
+			if (renegSupport > 0) {
+				if (FirstHandshakeDone) {
+					exv = new byte[24];
+					Array.Copy(savedClientFinished, 0,
+						exv, 0, 12);
+					Array.Copy(savedServerFinished, 0,
+						exv, 12, 12);
+				} else {
+					exv = new byte[0];
+				}
+			}
+			if (GetQuirkBool("forceEmptySecureReneg")) {
+				exv = new byte[0];
+			} else if (GetQuirkBool("forceNonEmptySecureReneg")) {
+				exv = new byte[24];
+			} else if (GetQuirkBool("alterNonEmptySecureReneg")) {
+				if (exv.Length > 0) {
+					exv[exv.Length - 1] ^= 0x01;
+				}
+			} else if (GetQuirkBool("oversizedSecureReneg")) {
+				exv = new byte[255];
+			}
+
+			if (exv != null) {
+				IO.Write16(chExt, 0xFF01);
+				IO.Write16(chExt, exv.Length + 1);
+				chExt.WriteByte((byte)exv.Length);
+				chExt.Write(exv, 0, exv.Length);
+			}
+		}
+
+		// Extra extension with random contents.
+		int extraExt = GetQuirkInt("sendExtraExtension", -1);
+		if (extraExt >= 0) {
+			byte[] exv = new byte[extraExt >> 16];
+			RNG.GetBytes(exv);
+			IO.Write16(chExt, extraExt & 0xFFFF);
+			IO.Write16(chExt, exv.Length);
+			chExt.Write(exv, 0, exv.Length);
+		}
+
+		// Max Fragment Length
+		// ALPN
+		// FIXME
+
+		byte[] encExt = chExt.ToArray();
+		if (encExt.Length > 0) {
+			if (encExt.Length > 65535) {
+				throw new SSLException("Oversized extensions");
+			}
+			IO.Write16(ms, encExt.Length);
+			ms.Write(encExt, 0, encExt.Length);
+		}
+
+		EndHandshakeMessage(ms);
+	}
+
+	void SendCertificate()
+	{
+		MemoryStream ms = StartHandshakeMessage(SSL.CERTIFICATE);
+		byte[][] chain = serverChoices.GetCertificateChain();
+		int tlen = 0;
+		foreach (byte[] ec in chain) {
+			tlen += 3 + ec.Length;
+		}
+		if (tlen > 0xFFFFFC) {
+			throw new SSLException("Oversized certificate chain");
+		}
+		IO.Write24(ms, tlen);
+		foreach (byte[] ec in chain) {
+			IO.Write24(ms, ec.Length);
+			ms.Write(ec, 0, ec.Length);
+		}
+		EndHandshakeMessage(ms);
+	}
+
+	void SendServerKeyExchange()
+	{
+		if (CommonCurves.Count == 0) {
+			/*
+			 * Since we filter cipher suites when parsing the
+			 * ClientHello, this situation may happen only if
+			 * the IServerPolicy callback goofed up.
+			 */
+			throw new SSLException("No curve for ECDHE");
+		}
+		int curveID = CommonCurves[0];
+		ecdheCurve = SSL.GetCurveByID(curveID);
+
+		/*
+		 * Generate our ephemeral ECDH secret and the point to
+		 * send to the peer.
+		 */
+		ecdheSecret = ecdheCurve.MakeRandomSecret();
+		byte[] P = ecdheCurve.GetGenerator(false);
+		ecdheCurve.Mul(P, ecdheSecret, P, false);
+
+		/*
+		 * Generate to-be-signed:
+		 *   clientRandom   32 bytes
+		 *   serverRandom   32 bytes
+		 *   0x03           curve is a "named curve"
+		 *   id             curve identifier (two bytes)
+		 *   point          public point (one-byte length + value)
+		 */
+		byte[] tbs = new byte[64 + 4 + P.Length];
+		Array.Copy(clientRandom, 0, tbs, 0, 32);
+		Array.Copy(serverRandom, 0, tbs, 32, 32);
+		tbs[64] = 0x03;
+		IO.Enc16be(curveID, tbs, 65);
+		tbs[67] = (byte)P.Length;
+		Array.Copy(P, 0, tbs, 68, P.Length);
+
+		/*
+		 * Obtain server signature.
+		 */
+		int hashAlgo, sigAlgo;
+		byte[] sig = serverChoices.DoSign(tbs,
+			out hashAlgo, out sigAlgo);
+
+		/*
+		 * Encode message.
+		 */
+		MemoryStream ms = StartHandshakeMessage(
+			SSL.SERVER_KEY_EXCHANGE);
+		ms.Write(tbs, 64, tbs.Length - 64);
+		if (Version >= SSL.TLS12) {
+			ms.WriteByte((byte)hashAlgo);
+			ms.WriteByte((byte)sigAlgo);
+		}
+		IO.Write16(ms, sig.Length);
+		ms.Write(sig, 0, sig.Length);
+		EndHandshakeMessage(ms);
+	}
+
+	void SendServerHelloDone()
+	{
+		MemoryStream ms = StartHandshakeMessage(SSL.SERVER_HELLO_DONE);
+		EndHandshakeMessage(ms);
+	}
+
+	void ParseClientKeyExchange()
+	{
+		byte[] msg = ReadHandshakeMessageExpected(
+			SSL.CLIENT_KEY_EXCHANGE);
+		byte[] pms;
+		if (SSL.IsECDHE(CipherSuite)) {
+			/*
+			 * Expecting a curve point; we are doing the
+			 * ECDH ourselves.
+			 */
+			if (msg.Length < 1 || msg.Length != 1 + msg[0]) {
+				throw new SSLException(
+					"Invalid ClientKeyExchange");
+			}
+			byte[] P = new byte[msg.Length - 1];
+			byte[] D = new byte[ecdheCurve.EncodedLength];
+			Array.Copy(msg, 1, P, 0, P.Length);
+			if (ecdheCurve.Mul(P, ecdheSecret, D, false) == 0) {
+				throw new SSLException(
+					"Invalid ClientKeyExchange");
+			}
+			int xlen;
+			int xoff = ecdheCurve.GetXoff(out xlen);
+			pms = new byte[xlen];
+			Array.Copy(D, xoff, pms, 0, xlen);
+
+			/*
+			 * Memory wiping is out of scope for this library,
+			 * and is unreliable anyway in the presence of
+			 * a moving garbage collector. So we just unlink
+			 * the secret array.
+			 */
+			ecdheSecret = null;
+		} else {
+			/*
+			 * RSA or static ECDH. The crypto operation is done
+			 * by the relevant callback.
+			 */
+			if (msg.Length < 2) {
+				throw new SSLException(
+					"Invalid ClientKeyExchange");
+			}
+			int off, len;
+			if (SSL.IsRSA(CipherSuite)) {
+				len = IO.Dec16be(msg, 0);
+				off = 2;
+			} else if (SSL.IsECDH(CipherSuite)) {
+				len = msg[0];
+				off = 1;
+			} else {
+				throw new Exception("NYI");
+			}
+			if (msg.Length != off + len) {
+				throw new SSLException(
+					"Invalid ClientKeyExchange");
+			}
+			byte[] cke = new byte[len];
+			Array.Copy(msg, off, cke, 0, len);
+			pms = serverChoices.DoKeyExchange(cke);
+		}
+
+		ComputeMaster(pms);
+	}
+
+	internal override void ProcessExtraHandshake()
+	{
+		/*
+		 * If Secure Renegotiation is supported, then we accept
+		 * to do a new handshake.
+		 */
+		if (renegSupport > 0) {
+			DoHandshake();
+			return;
+		}
+
+		/*
+		 * We must read and discard an incoming ClientHello,
+		 * then politely refuse.
+		 */
+		ReadHandshakeMessageExpected(SSL.CLIENT_HELLO);
+		SendWarning(SSL.NO_RENEGOTIATION);
+		SetAppData();
+	}
+
+	internal override void PrepareRenegotiate()
+	{
+		MemoryStream ms = StartHandshakeMessage(SSL.HELLO_REQUEST);
+		EndHandshakeMessage(ms);
+		FlushSub();
+	}
+
+	/*
+	 * Compute the intersection of two lists of integers (the second
+	 * list is provided as an array). The intersection is returned
+	 * as a new List<int> instance. If enforceV2 is true, then the
+	 * order of items in the returned list will be that of v2; otherwise,
+	 * it will be that of v1. Duplicates are removed.
+	 */
+	static List<int> FilterList(List<int> v1, int[] v2, bool enforceV2)
+	{
+		List<int> r = new List<int>();
+		if (enforceV2) {
+			foreach (int x in v2) {
+				if (v1.Contains(x) && !r.Contains(x)) {
+					r.Add(x);
+				}
+			}
+		} else {
+			foreach (int x in v1) {
+				foreach (int y in v2) {
+					if (x == y) {
+						if (!r.Contains(x)) {
+							r.Add(x);
+						}
+						break;
+					}
+				}
+			}
+		}
+		return r;
+	}
+}
+
+}
diff --git a/SSLTLS/SSLServerPolicyBasic.cs b/SSLTLS/SSLServerPolicyBasic.cs
new file mode 100644
index 0000000..d4bf1db
--- /dev/null
+++ b/SSLTLS/SSLServerPolicyBasic.cs
@@ -0,0 +1,358 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+using Crypto;
+
+namespace SSLTLS {
+
+/*
+ * Basic implementation of IServerPolicy: it uses a single certificate
+ * chain and a software private key.
+ */
+
+public class SSLServerPolicyBasic : IServerPolicy {
+
+	byte[][] chain;
+	IPrivateKey skey;
+	bool canSign, canEncrypt;
+
+	/*
+	 * Create the policy instance, with the provided certificate chain,
+	 * private key, and allowed key usages.
+	 */
+	public SSLServerPolicyBasic(byte[][] chain,
+		IPrivateKey skey, KeyUsage usage)
+	{
+		this.chain = chain;
+		this.skey = skey;
+		canEncrypt = false;
+		canSign = false;
+		switch (usage) {
+		case KeyUsage.EncryptOnly:
+			canEncrypt = true;
+			break;
+		case KeyUsage.SignOnly:
+			canSign = true;
+			break;
+		case KeyUsage.EncryptAndSign:
+			canEncrypt = true;
+			canSign = true;
+			break;
+		}
+	}
+
+	public IServerChoices Apply(SSLServer server)
+	{
+		/*
+		 * Conditions for selecting a cipher suite:
+		 *
+		 *   RSA           canEncrypt, key is RSA
+		 *
+		 *   ECDH          canEncrypt, key is EC, curve supported
+		 *
+		 *   ECDHE_RSA     canSign, key is RSA, have hash+RSA algo
+		 *
+		 *   ECDHE_ECDSA   canSign, key is EC, have hash+ECDSA algo,
+		 *                 curve supported
+		 *
+		 * The engine already filtered things, so we know that:
+		 *
+		 *  - if an ECDHE suite is present, then there is a common
+		 *    supported curve;
+		 *
+		 *  - if an ECDHE_RSA suite is present, then there is a
+		 *    common hash+RSA algorithm;
+		 *
+		 *  - if an ECDHE_ECDSA suite is present, then there is a
+		 *    common hash+ECDSA algorithm.
+		 *
+		 * We must still walk the list of algorithm to determine the
+		 * proper hash to use for signatures; we also need to check
+		 * that our EC curve is supported by the client.
+		 */
+		int curveID = -1;
+		if (skey is ECPrivateKey) {
+			curveID = SSL.CurveToID(((ECPrivateKey)skey).Curve);
+		}
+		bool canRSA = canEncrypt && (skey is RSAPrivateKey);
+		bool canECDH = canEncrypt && (skey is ECPrivateKey)
+			&& server.ClientCurves.Contains(curveID);
+		bool canECDHE_RSA = canSign && (skey is RSAPrivateKey);
+		bool canECDHE_ECDSA = canSign && (skey is ECPrivateKey);
+
+		foreach (int cs in server.CommonCipherSuites) {
+			if (SSL.IsRSA(cs)) {
+				if (!canRSA) {
+					continue;
+				}
+				return new ChoicesRSA(server.ClientVersionMax,
+					cs, chain, skey as RSAPrivateKey);
+			} else if (SSL.IsECDH(cs)) {
+				if (!canECDH) {
+					continue;
+				}
+				return new ChoicesECDH(cs, chain,
+					skey as ECPrivateKey);
+			} else if (SSL.IsECDHE_RSA(cs)) {
+				if (!canECDHE_RSA) {
+					continue;
+				}
+				int hashAlgo;
+				if (server.Version <= SSL.TLS11) {
+					hashAlgo = SSL.MD5SHA1;
+				} else {
+					hashAlgo = SelectHash(
+						server.ClientHashAndSign,
+						SSL.RSA);
+				}
+				return new ChoicesSign(cs, chain,
+					hashAlgo, skey);
+			} else if (SSL.IsECDHE_ECDSA(cs)) {
+				if (!canECDHE_ECDSA) {
+					continue;
+				}
+				int hashAlgo;
+				if (server.Version <= SSL.TLS11) {
+					hashAlgo = SSL.SHA1;
+				} else {
+					hashAlgo = SelectHash(
+						server.ClientHashAndSign,
+						SSL.ECDSA);
+				}
+				return new ChoicesSign(cs, chain,
+					hashAlgo, skey);
+			}
+		}
+
+		throw new SSLException("No suitable cipher suite");
+	}
+
+	static int SelectHash(List<int> hsl, int sigAlg)
+	{
+		foreach (int x in hsl) {
+			if ((x & 0xFF) == sigAlg) {
+				return x >> 8;
+			}
+		}
+
+		/*
+		 * This should never happen, because the offending
+		 * cipher suites would have been filtered by the engine.
+		 */
+		throw new Exception();
+	}
+}
+
+class ChoicesBase {
+
+	int cipherSuite;
+	byte[][] chain;
+
+	internal ChoicesBase(int cipherSuite, byte[][] chain)
+	{
+		this.cipherSuite = cipherSuite;
+		this.chain = chain;
+	}
+
+	public int GetCipherSuite()
+	{
+		return cipherSuite;
+	}
+
+	public byte[][] GetCertificateChain()
+	{
+		return chain;
+	}
+
+	public virtual byte[] DoKeyExchange(byte[] cke)
+	{
+		throw new Exception();
+	}
+
+	public virtual byte[] DoSign(byte[] ske,
+		out int hashAlgo, out int sigAlgo)
+	{
+		throw new Exception();
+	}
+}
+
+class ChoicesRSA : ChoicesBase, IServerChoices {
+
+	int clientVersionMax;
+	RSAPrivateKey rk;
+
+	internal ChoicesRSA(int clientVersionMax, int cipherSuite,
+		byte[][] chain, RSAPrivateKey rk)
+		: base(cipherSuite, chain)
+	{
+		this.clientVersionMax = clientVersionMax;
+		this.rk = rk;
+	}
+
+	public override byte[] DoKeyExchange(byte[] cke)
+	{
+		if (cke.Length < 59) {
+			throw new CryptoException(
+				"Invalid ClientKeyExchange (too short)");
+		}
+		RSA.DoPrivate(rk, cke);
+
+		/*
+		 * Constant-time check for PKCS#1 v1.5 padding. z is set
+		 * to -1 if the padding is correct, 0 otherwise. We also
+		 * check the two first PMS byte (they should be equal to
+		 * the maximum protocol version announced by the client
+		 * in its ClientHello).
+		 */
+		int z = 0;
+		z |= cke[0];
+		z |= cke[1] ^ 0x02;
+		for (int i = 2; i < cke.Length - 49; i ++) {
+			int y = cke[i];
+			z |= ~((y | -y) >> 31);
+		}
+		z |= cke[cke.Length - 49];
+		z |= cke[cke.Length - 48] ^ (clientVersionMax >> 8);
+		z |= cke[cke.Length - 47] ^ (clientVersionMax & 0xFF);
+		z = ~((z | -z) >> 31);
+
+		/*
+		 * Get a random premaster, then overwrite it with the
+		 * decrypted value, but only if the padding was correct.
+		 */
+		byte[] pms = new byte[48];
+		RNG.GetBytes(pms);
+		for (int i = 0; i < 48; i ++) {
+			int x = pms[i];
+			int y = cke[cke.Length - 48 + i];
+			pms[i] = (byte)(x ^ (z & (x ^ y)));
+		}
+
+		return pms;
+	}
+}
+
+class ChoicesECDH : ChoicesBase, IServerChoices {
+
+	ECPrivateKey ek;
+
+	internal ChoicesECDH(int cipherSuite, byte[][] chain, ECPrivateKey ek)
+		: base(cipherSuite, chain)
+	{
+		this.ek = ek;
+	}
+
+	public override byte[] DoKeyExchange(byte[] cke)
+	{
+		ECCurve curve = ek.Curve;
+		byte[] tmp = new byte[curve.EncodedLength];
+		if (curve.Mul(cke, ek.X, tmp, false) == 0) {
+			throw new SSLException(
+				"Invalid ClientKeyExchange EC point value");
+		}
+		int xlen;
+		int xoff = curve.GetXoff(out xlen);
+		byte[] pms = new byte[xlen];
+		Array.Copy(tmp, xoff, pms, 0, xlen);
+		return pms;
+	}
+}
+
+class ChoicesSign : ChoicesBase, IServerChoices {
+
+	int hashAlgo;
+	IPrivateKey skey;
+
+	internal ChoicesSign(int cipherSuite, byte[][] chain,
+		int hashAlgo, IPrivateKey skey)
+		: base(cipherSuite, chain)
+	{
+		this.hashAlgo = hashAlgo;
+		this.skey = skey;
+	}
+
+	public override byte[] DoSign(byte[] ske,
+		out int hashAlgo, out int signAlgo)
+	{
+		hashAlgo = this.hashAlgo;
+		byte[] hv = Hash(hashAlgo, ske);
+		if (skey is RSAPrivateKey) {
+			RSAPrivateKey rk = skey as RSAPrivateKey;
+			signAlgo = SSL.RSA;
+			byte[] head;
+			switch (hashAlgo) {
+			case SSL.MD5SHA1: head = null; break;
+			case SSL.SHA1:    head = RSA.PKCS1_SHA1; break;
+			case SSL.SHA224:  head = RSA.PKCS1_SHA224; break;
+			case SSL.SHA256:  head = RSA.PKCS1_SHA256; break;
+			case SSL.SHA384:  head = RSA.PKCS1_SHA384; break;
+			case SSL.SHA512:  head = RSA.PKCS1_SHA512; break;
+			default:
+				throw new Exception();
+			}
+			return RSA.Sign(rk, head, hv);
+		} else if (skey is ECPrivateKey) {
+			ECPrivateKey ek = skey as ECPrivateKey;
+			signAlgo = SSL.ECDSA;
+			return ECDSA.Sign(ek, null, hv);
+		} else {
+			throw new Exception("NYI");
+		}
+	}
+
+	static byte[] Hash(int hashAlgo, byte[] data)
+	{
+		switch (hashAlgo) {
+		case SSL.MD5SHA1:
+			byte[] hv = new byte[36];
+			MD5 md5 = new MD5();
+			SHA1 sha1 = new SHA1();
+			md5.Update(data);
+			md5.DoFinal(hv, 0);
+			sha1.Update(data);
+			sha1.DoFinal(hv, 16);
+			return hv;
+		case SSL.MD5:
+			return new MD5().Hash(data);
+		case SSL.SHA1:
+			return new SHA1().Hash(data);
+		case SSL.SHA224:
+			return new SHA224().Hash(data);
+		case SSL.SHA256:
+			return new SHA256().Hash(data);
+		case SSL.SHA384:
+			return new SHA384().Hash(data);
+		case SSL.SHA512:
+			return new SHA512().Hash(data);
+		default:
+			throw new Exception("NYI");
+		}
+	}
+}
+
+}
diff --git a/SSLTLS/SSLSessionCacheLRU.cs b/SSLTLS/SSLSessionCacheLRU.cs
new file mode 100644
index 0000000..916638d
--- /dev/null
+++ b/SSLTLS/SSLSessionCacheLRU.cs
@@ -0,0 +1,119 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace SSLTLS {
+
+/*
+ * A basic implementation of an SSL session cache. It stores up to a
+ * predetermined number of sessions, and uses a least-recently-used
+ * eviction policy. Sessions are kept in RAM.
+ *
+ * Instances use locking to be thread-safe, so that multiple SSL server
+ * engines managed in different threads may share the same cache (though
+ * each engine, individually, is not thread-safe).
+ */
+
+public class SSLSessionCacheLRU : ISessionCache {
+
+	/*
+	 * TODO: use a doubly-linked list for LRU policy. Right now,
+	 * values are indexed in an array, and eviction/moving implies
+	 * shifting many pointers in that array. This is fine for small
+	 * caches, up to a few thousands of entries.
+	 */
+
+	object mutex;
+	IDictionary<string, int> spx;
+	SSLSessionParameters[] data;
+	int count, maxCount;
+
+	public SSLSessionCacheLRU(int maxCount)
+	{
+		spx = new Dictionary<string, int>();
+		data = new SSLSessionParameters[maxCount];
+		count = 0;
+		this.maxCount = maxCount;
+		mutex = new object();
+	}
+
+	/* see ISessionCache */
+	public SSLSessionParameters Retrieve(byte[] id, string serverName)
+	{
+		lock (mutex) {
+			int x;
+			if (!spx.TryGetValue(IDToString(id), out x)) {
+				return null;
+			}
+			SSLSessionParameters sp = data[x];
+			if ((x + 1) < count) {
+				Array.Copy(data, x + 1,	
+					data, x, count - x - 1);
+				data[count - 1] = sp;
+			}
+			return sp;
+		}
+	}
+
+	/* see ISessionCache */
+	public void Store(SSLSessionParameters sp)
+	{
+		lock (mutex) {
+			string ids = IDToString(sp.SessionID);
+			int x;
+			if (spx.TryGetValue(ids, out x)) {
+				if ((x + 1) < count) {
+					Array.Copy(data, x + 1,
+						data, x, count - x - 1);
+				}
+				spx[ids] = count - 1;
+				data[count - 1] = sp;
+				return;
+			}
+			if (count == maxCount) {
+				SSLSessionParameters esp = data[0];
+				Array.Copy(data, 1, data, 0, count - 1);
+				count --;
+				spx.Remove(IDToString(esp.SessionID));
+			}
+			spx[ids] = count;
+			data[count] = sp;
+			count ++;
+		}
+	}
+
+	static string IDToString(byte[] id)
+	{
+		StringBuilder sb = new StringBuilder();
+		foreach (byte b in id) {
+			sb.AppendFormat("{0:x2}", b);
+		}
+		return sb.ToString();
+	}
+}
+
+}
diff --git a/SSLTLS/SSLSessionParameters.cs b/SSLTLS/SSLSessionParameters.cs
new file mode 100644
index 0000000..a1e0c03
--- /dev/null
+++ b/SSLTLS/SSLSessionParameters.cs
@@ -0,0 +1,88 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+namespace SSLTLS {
+
+/*
+ * An SSLSessionParameters instance contains the "security parameters"
+ * for a SSL session. Instances are obtained from an open instance
+ * (after the handshake) and can be used to initialise new instances
+ * so as to attempt a session resumption.
+ */
+
+public class SSLSessionParameters {
+
+	/*
+	 * Session ID (at most 32 bytes).
+	 */
+	public byte[] SessionID {
+		get; set;
+	}
+
+	/*
+	 * Protocol version.
+	 */
+	public int Version {
+		get; set;
+	}
+
+	/*
+	 * Used cipher suite.
+	 */
+	public int CipherSuite {
+		get; set;
+	}
+
+	/*
+	 * Server name attached to this session; it may be null.
+	 */
+	public string ServerName {
+		get; set;
+	}
+
+	/*
+	 * Negotiated master secret.
+	 */
+	public byte[] MasterSecret {
+		get; set;
+	}
+
+	/*
+	 * Create a new instance. The provided sessionID and masterSecret
+	 * arrays are internally copied into new instances.
+	 */
+	public SSLSessionParameters(byte[] sessionID, int version,
+		int cipherSuite, string serverName, byte[] masterSecret)
+	{
+		SessionID = IO.CopyBlob(sessionID);
+		Version = version;
+		CipherSuite = cipherSuite;
+		ServerName = serverName;
+		MasterSecret = IO.CopyBlob(masterSecret);
+	}
+}
+
+}
diff --git a/Tests/Poly1305Ref.cs b/Tests/Poly1305Ref.cs
new file mode 100644
index 0000000..a1d0420
--- /dev/null
+++ b/Tests/Poly1305Ref.cs
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+using Crypto;
+
+/*
+ * This is a "reference" implementation of Poly1305 that uses the
+ * generic ZInt code for computations. It is not constant-time, and
+ * it is very slow; it is meant to test other implementations.
+ *
+ * API is identical to the Poly1305 class.
+ */
+
+public class Poly1305Ref {
+
+	public ChaCha20 ChaCha {
+		get; set;
+	}
+
+	public Poly1305Ref()
+	{
+	}
+
+	static ZInt p = ((ZInt)1 << 130) - (ZInt)5;
+	static ZInt rmask = ((ZInt)1 << 124) - (ZInt)1
+		- ((ZInt)15 << 28) - ((ZInt)15 << 60) - ((ZInt)15 << 92)
+		- ((ZInt)3 << 32) - ((ZInt)3 << 64) - ((ZInt)3 << 96);
+
+	public void Run(byte[] iv,
+		byte[] data, int off, int len,
+		byte[] aad, int offAAD, int lenAAD,
+		byte[] tag, bool encrypt)
+	{
+		byte[] pkey = new byte[32];
+		ChaCha.Run(iv, 0, pkey);
+		if (encrypt) {
+			ChaCha.Run(iv, 1, data, off, len);
+		}
+
+		ByteSwap(pkey, 0, 16);
+		ZInt r = ZInt.DecodeUnsignedBE(pkey, 0, 16);
+		r &= rmask;
+		ZInt a = (ZInt)0;
+
+		a = RunInner(a, r, aad, offAAD, lenAAD);
+		a = RunInner(a, r, data, off, len);
+		byte[] foot = new byte[16];
+		foot[ 0] = (byte)lenAAD;
+		foot[ 1] = (byte)(lenAAD >> 8);
+		foot[ 2] = (byte)(lenAAD >> 16);
+		foot[ 3] = (byte)(lenAAD >> 24);
+		foot[ 8] = (byte)len;
+		foot[ 9] = (byte)(len >> 8);
+		foot[10] = (byte)(len >> 16);
+		foot[11] = (byte)(len >> 24);
+		a = RunInner(a, r, foot, 0, 16);
+
+		ByteSwap(pkey, 16, 16);
+		ZInt s = ZInt.DecodeUnsignedBE(pkey, 16, 16);
+		a += s;
+		a.ToBytesLE(tag, 0, 16);
+
+		if (!encrypt) {
+			ChaCha.Run(iv, 1, data, off, len);
+		}
+	}
+
+	ZInt RunInner(ZInt a, ZInt r, byte[] data, int off, int len)
+	{
+		byte[] tmp = new byte[16];
+		while (len > 0) {
+			if (len >= 16) {
+				Array.Copy(data, off, tmp, 0, 16);
+			} else {
+				Array.Copy(data, off, tmp, 0, len);
+				for (int i = len; i < 16; i ++) {
+					tmp[i] = 0;
+				}
+			}
+			ByteSwap(tmp, 0, 16);
+			ZInt v = ZInt.DecodeUnsignedBE(tmp) | ((ZInt)1 << 128);
+			a = ((a + v) * r) % p;
+			off += 16;
+			len -= 16;
+		}
+		return a;
+	}
+
+	static void ByteSwap(byte[] buf, int off, int len)
+	{
+		for (int i = 0; (i + i) < len; i ++) {
+			byte t = buf[off + i];
+			buf[off + i] = buf[off + len - 1 - i];
+			buf[off + len - 1 - i] = t;
+		}
+	}
+}
diff --git a/Tests/TestCrypto.cs b/Tests/TestCrypto.cs
new file mode 100644
index 0000000..cbcff5f
--- /dev/null
+++ b/Tests/TestCrypto.cs
@@ -0,0 +1,3843 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+using Crypto;
+
+public class TestCrypto {
+
+	public static void Main(string[] args)
+	{
+		IDictionary<string, bool> d =
+			new SortedDictionary<string, bool>(
+				StringComparer.OrdinalIgnoreCase);
+		foreach (string a in args) {
+			StringBuilder sb = new StringBuilder();
+			foreach (char c in a.ToLowerInvariant()) {
+				if ((c >= 'a' && c <= 'z')
+					|| (c >= '0' && c <= '9'))
+				{
+					sb.Append(c);
+				}
+			}
+			d[sb.ToString()] = true;
+		}
+		bool all = (d.Count == 0);
+		try {
+			if (all || d.ContainsKey("md5")) {
+				TestMD5();
+			}
+			if (all || d.ContainsKey("sha1")) {
+				TestSHA1();
+			}
+			if (all || d.ContainsKey("sha224")) {
+				TestSHA224();
+			}
+			if (all || d.ContainsKey("sha256")) {
+				TestSHA256();
+			}
+			if (all || d.ContainsKey("sha384")) {
+				TestSHA384();
+			}
+			if (all || d.ContainsKey("sha512")) {
+				TestSHA512();
+			}
+			if (all || d.ContainsKey("hmac")) {
+				TestHMAC();
+			}
+			if (all || d.ContainsKey("hmacdrbg")) {
+				TestHMAC_DRBG();
+			}
+			if (all || d.ContainsKey("aes")) {
+				TestAES();
+			}
+			if (all || d.ContainsKey("des")) {
+				TestDES();
+			}
+			if (all || d.ContainsKey("chacha20")) {
+				TestChaCha20();
+			}
+			if (all || d.ContainsKey("poly1305")) {
+				TestPoly1305();
+			}
+			if (all || d.ContainsKey("ghash")) {
+				TestGHASH();
+			}
+			if (all || d.ContainsKey("int")) {
+				TestMath.TestModInt();
+			}
+			if (all || d.ContainsKey("rsa")) {
+				TestRSA();
+			}
+			if (all || d.ContainsKey("ec")) {
+				TestEC.TestECInt();
+			}
+			if (all || d.ContainsKey("ecdsa")) {
+				TestECDSA();
+			}
+		} catch (Exception e) {
+			Console.WriteLine(e.ToString());
+			Environment.Exit(1);
+		}
+	}
+
+	static void TestMD5()
+	{
+		Console.Write("Testing MD5... ");
+		DoKATHash(new MD5(), KAT_MD5);
+		Console.WriteLine("done.");
+	}
+
+	static void TestSHA1()
+	{
+		Console.Write("Testing SHA-1... ");
+		DoKATHash(new SHA1(), KAT_SHA1);
+		DoKATHashLong(new SHA1(), "34aa973cd4c4daa4f61eeb2bdbad27316534016f");
+		Console.WriteLine("done.");
+	}
+
+	static void TestSHA224()
+	{
+		Console.Write("Testing SHA-224... ");
+		DoKATHash(new SHA224(), KAT_SHA224);
+		DoKATHashLong(new SHA224(), "20794655980c91d8bbb4c1ea97618a4bf03f42581948b2ee4ee7ad67");
+		Console.WriteLine("done.");
+	}
+
+	static void TestSHA256()
+	{
+		Console.Write("Testing SHA-256... ");
+		DoKATHash(new SHA256(), KAT_SHA256);
+		DoKATHashLong(new SHA256(), "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0");
+		Console.WriteLine("done.");
+	}
+
+	static void TestSHA384()
+	{
+		Console.Write("Testing SHA-384... ");
+		DoKATHash(new SHA384(), KAT_SHA384);
+		DoKATHashLong(new SHA384(), "9d0e1809716474cb086e834e310a4a1ced149e9c00f248527972cec5704c2a5b07b8b3dc38ecc4ebae97ddd87f3d8985");
+		Console.WriteLine("done.");
+	}
+
+	static void TestSHA512()
+	{
+		Console.Write("Testing SHA-512... ");
+		DoKATHash(new SHA512(), KAT_SHA512);
+		DoKATHashLong(new SHA512(), "e718483d0ce769644e2e42c7bc15b4638e1f98b13b2044285632a803afa973ebde0ff244877ea60a4cb0432ce577c31beb009c5c2c49aa2e4eadb217ad8cc09b");
+		Console.WriteLine("done.");
+	}
+
+	static void DoKATHash(IDigest h, string[] katTab)
+	{
+		for (int i = 0; i < katTab.Length; i += 2) {
+			DoKATHash(h,
+				Encoding.UTF8.GetBytes(katTab[i]),
+				ToBin(katTab[i + 1]));
+		}
+	}
+
+	static void TestHMAC()
+	{
+		Console.Write("Testing HMAC... ");
+
+		DoKATHMAC(new HMAC(new MD5()),
+			ToBin("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"),
+			Encoding.UTF8.GetBytes("Hi There"),
+			ToBin("9294727a3638bb1c13f48ef8158bfc9d"));
+		DoKATHMAC(new HMAC(new MD5()),
+			Encoding.UTF8.GetBytes("Jefe"),
+			Encoding.UTF8.GetBytes("what do ya want for nothing?"),
+			ToBin("750c783e6ab0b503eaa86e310a5db738"));
+		DoKATHMAC(new HMAC(new MD5()),
+			ToBin("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
+			ToBin("DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"),
+			ToBin("56be34521d144c88dbb8c733f0e8b3f6"));
+		DoKATHMAC(new HMAC(new MD5()),
+			ToBin("0102030405060708090a0b0c0d0e0f10111213141516171819"),
+			ToBin("CDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCD"),
+			ToBin("697eaf0aca3a3aea3a75164746ffaa79"));
+		DoKATHMAC(new HMAC(new MD5()),
+			ToBin("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c"),
+			Encoding.UTF8.GetBytes("Test With Truncation"),
+			ToBin("56461ef2342edc00f9bab995690efd4c"));
+		DoKATHMAC(new HMAC(new MD5()),
+			ToBin("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
+			Encoding.UTF8.GetBytes("Test Using Larger Than Block-Size Key - Hash Key First"),
+			ToBin("6b1ab7fe4bd7bf8f0b62e6ce61b9d0cd"));
+		DoKATHMAC(new HMAC(new MD5()),
+			ToBin("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
+			Encoding.UTF8.GetBytes("Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data"),
+			ToBin("6f630fad67cda0ee1fb1f562db3aa53e"));
+
+		DoKATHMAC(new HMAC(new SHA1()),
+			ToBin("0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b"),
+			Encoding.UTF8.GetBytes("Hi There"),
+			ToBin("b617318655057264e28bc0b6fb378c8ef146be00"));
+		DoKATHMAC(new HMAC(new SHA1()),
+			Encoding.UTF8.GetBytes("Jefe"),
+			Encoding.UTF8.GetBytes("what do ya want for nothing?"),
+			ToBin("effcdf6ae5eb2fa2d27416d5f184df9c259a7c79"));
+		DoKATHMAC(new HMAC(new SHA1()),
+			ToBin("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
+			ToBin("DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD"),
+			ToBin("125d7342b9ac11cd91a39af48aa17b4f63f175d3"));
+		DoKATHMAC(new HMAC(new SHA1()),
+			ToBin("0102030405060708090a0b0c0d0e0f10111213141516171819"),
+			ToBin("CDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCDCD"),
+			ToBin("4c9007f4026250c6bc8414f9bf50c86c2d7235da"));
+		DoKATHMAC(new HMAC(new SHA1()),
+			ToBin("0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c"),
+			Encoding.UTF8.GetBytes("Test With Truncation"),
+			ToBin("4c1a03424b55e07fe7f27be1d58bb9324a9a5a04"));
+		DoKATHMAC(new HMAC(new SHA1()),
+			ToBin("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
+			Encoding.UTF8.GetBytes("Test Using Larger Than Block-Size Key - Hash Key First"),
+			ToBin("aa4ae5e15272d00e95705637ce8a3b55ed402112"));
+		DoKATHMAC(new HMAC(new SHA1()),
+			ToBin("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
+			Encoding.UTF8.GetBytes("Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data"),
+			ToBin("e8e99d0f45237d786d6bbaa7965c7808bbff1a91"));
+
+		TestHMAC_CT(new MD5());
+		TestHMAC_CT(new SHA1());
+		TestHMAC_CT(new SHA256());
+		TestHMAC_CT(new SHA512());
+
+		Console.WriteLine("done.");
+	}
+
+	static void TestHMAC_CT(IDigest h)
+	{
+		Console.Write("[{0}]", h.Name);
+		int hlen = h.DigestSize;
+		HMAC hm = new HMAC(h);
+		byte[] key = new byte[hlen];
+		RNG.GetBytes(key);
+		hm.SetKey(key);
+		byte[] data = new byte[384];
+		RNG.GetBytes(data);
+		byte[] tmp1 = new byte[hlen];
+		byte[] tmp2 = new byte[hlen];
+		for (int i = 0; i < 256; i ++) {
+			int i1 = i >> 1;
+			int i2 = i - i1;
+			for (int j = 0; j <= 128; j ++) {
+				hm.Update(data, 0, i1);
+				hm.ComputeCT(data, i1, i2 + j, i2, i2 + j,
+					tmp1, 0);
+				hm.Update(data, 0, i + j);
+				hm.DoFinal(tmp2, 0);
+				CheckEq(tmp1, tmp2, "CT");
+			}
+			Console.Write(".");
+		}
+	}
+
+	static void TestHMAC_DRBG()
+	{
+		Console.Write("Testing HMAC_DRBG... ");
+		byte[] tmp = new byte[30];
+
+		HMAC_DRBG drbg = new HMAC_DRBG(new SHA256());
+		drbg.Update(ToBin("009A4D6792295A7F730FC3F2B49CBC0F62E862272F01795EDF0D54DB760F156D0DAC04C0322B3A204224"));
+		drbg.GetBytes(tmp);
+		CheckEq(tmp, ToBin("9305A46DE7FF8EB107194DEBD3FD48AA20D5E7656CBE0EA69D2A8D4E7C67"), "KAT 1");
+		drbg.GetBytes(tmp);
+		CheckEq(tmp, ToBin("C70C78608A3B5BE9289BE90EF6E81A9E2C1516D5751D2F75F50033E45F73"), "KAT 2");
+		drbg.GetBytes(tmp);
+		CheckEq(tmp, ToBin("475E80E992140567FCC3A50DAB90FE84BCD7BB03638E9C4656A06F37F650"), "KAT 3");
+
+		Console.WriteLine("done.");
+	}
+
+	static void TestAES()
+	{
+		Console.Write("Testing AES... ");
+
+		AES bc = new AES();
+		DoKATBlockCipherRaw(bc, KAT_AES_RAW);
+		DoKATBlockCipherCBC(bc, KAT_AES_CBC);
+		DoKATBlockCipherCTR(bc, KAT_AES_CTR);
+
+		DoMonteCarloAESEncrypt(bc,
+			"139a35422f1d61de3c91787fe0507afd",
+			"b9145a768b7dc489a096b546f43b231f",
+			"fb2649694783b551eacd9d5db6126d47");
+		DoMonteCarloAESDecrypt(bc,
+			"0c60e7bf20ada9baa9e1ddf0d1540726",
+			"b08a29b11a500ea3aca42c36675b9785",
+			"d1d2bfdc58ffcad2341b095bce55221e");
+
+		DoMonteCarloAESEncrypt(bc,
+			"b9a63e09e1dfc42e93a90d9bad739e5967aef672eedd5da9",
+			"85a1f7a58167b389cddc8a9ff175ee26",
+			"5d1196da8f184975e240949a25104554");
+		DoMonteCarloAESDecrypt(bc,
+			"4b97585701c03fbebdfa8555024f589f1482c58a00fdd9fd",
+			"d0bd0e02ded155e4516be83f42d347a4",
+			"b63ef1b79507a62eba3dafcec54a6328");
+
+		DoMonteCarloAESEncrypt(bc,
+			"f9e8389f5b80712e3886cc1fa2d28a3b8c9cd88a2d4a54c6aa86ce0fef944be0",
+			"b379777f9050e2a818f2940cbbd9aba4",
+			"c5d2cb3d5b7ff0e23e308967ee074825");
+		DoMonteCarloAESDecrypt(bc,
+			"2b09ba39b834062b9e93f48373b8dd018dedf1e5ba1b8af831ebbacbc92a2643",
+			"89649bd0115f30bd878567610223a59d",
+			"e3d3868f578caf34e36445bf14cefc68");
+
+		Console.WriteLine("done.");
+	}
+
+	static void TestDES()
+	{
+		Console.Write("Testing DES... ");
+
+		DES bc = new DES();
+		DoKATBlockCipherRaw(bc, KAT_DES_RAW);
+		DoKATBlockCipherCBC(bc, KAT_DES_CBC);
+		DoMonteCarloDESEncrypt(bc);
+		DoMonteCarloDESDecrypt(bc);
+
+		Console.WriteLine("done.");
+	}
+
+	static void TestChaCha20()
+	{
+		Console.Write("Testing ChaCha20... ");
+
+		for (int i = 0; i < KAT_CHACHA20.Length; i += 5) {
+			byte[] key = ToBin(KAT_CHACHA20[i + 0]);
+			byte[] iv = ToBin(KAT_CHACHA20[i + 1]);
+			uint cc = UInt32.Parse(KAT_CHACHA20[i + 2]);
+			byte[] plain = ToBin(KAT_CHACHA20[i + 3]);
+			byte[] cipher = ToBin(KAT_CHACHA20[i + 4]);
+
+			ChaCha20 chacha = new ChaCha20();
+			chacha.SetKey(key);
+			byte[] tmp = new byte[plain.Length];
+
+			for (int j = 0; j <= plain.Length; j ++) {
+				for (int k = 0; k < tmp.Length; k ++) {
+					tmp[k] = 0;
+				}
+				Array.Copy(plain, 0, tmp, 0, j);
+				if (chacha.Run(iv, cc, tmp, 0, j)
+					!= cc + (uint)((j + 63) >> 6))
+				{
+					throw new Exception(
+						"ChaCha20: wrong counter");
+				}
+				CheckEq(tmp, 0, cipher, 0, j, "KAT 1");
+				for (int k = j; k < tmp.Length; k ++) {
+					if (tmp[k] != 0) {
+						throw new Exception(
+							"ChaCha20: overrun");
+					}
+				}
+
+				uint cc2 = cc;
+				for (int k = 0; k < j; k += 64) {
+					int clen = Math.Min(64, j - k);
+					cc2 = chacha.Run(iv, cc2, tmp, k, clen);
+				}
+				CheckEq(tmp, 0, plain, 0, j, "KAT 2");
+			}
+		}
+
+		Console.WriteLine("done.");
+	}
+
+	static void TestPoly1305()
+	{
+		Console.Write("Testing Poly1305... ");
+
+		byte[] plain = ToBin("4c616469657320616e642047656e746c656d656e206f662074686520636c617373206f66202739393a204966204920636f756c64206f6666657220796f75206f6e6c79206f6e652074697020666f7220746865206675747572652c2073756e73637265656e20776f756c642062652069742e");
+		byte[] aad = ToBin("50515253c0c1c2c3c4c5c6c7");
+		byte[] key = ToBin("808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9f");
+		byte[] iv = ToBin("070000004041424344454647");
+		byte[] cipher = ToBin("d31a8d34648e60db7b86afbc53ef7ec2a4aded51296e08fea9e2b5a736ee62d63dbea45e8ca9671282fafb69da92728b1a71de0a9e060b2905d6a5b67ecd3b3692ddbd7f2d778b8c9803aee328091b58fab324e4fad675945585808b4831d7bc3ff4def08e4b7a9de576d26586cec64b6116");
+		byte[] tag = ToBin("1ae10b594f09e26a7e902ecbd0600691");
+
+		byte[] data = new byte[plain.Length];
+		Array.Copy(plain, 0, data, 0, plain.Length);
+		byte[] tt = new byte[16];
+
+		ChaCha20 cc = new ChaCha20();
+		cc.SetKey(key);
+		Poly1305 pp = new Poly1305();
+		pp.ChaCha = cc;
+		pp.Run(iv, data, 0, data.Length, aad, 0, aad.Length, tt, true);
+		CheckEq(data, cipher, "KAT 1");
+		CheckEq(tt, tag, "KAT 2");
+		for (int i = 0; i < tt.Length; i ++) {
+			tt[i] = 0;
+		}
+		pp.Run(iv, data, 0, data.Length, aad, 0, aad.Length, tt, false);
+		CheckEq(data, plain, "KAT 3");
+		CheckEq(tt, tag, "KAT 4");
+
+		/*
+		 * Make random messages and keys, and test the implementation
+		 * against the ZInt-based reference code.
+		 */
+		Poly1305Ref ppref = new Poly1305Ref();
+		ppref.ChaCha = new ChaCha20();
+		data = new byte[100];
+		aad = new byte[data.Length];
+		byte[] tmp = new byte[data.Length];
+		key = new byte[32];
+		iv = new byte[12];
+		byte[] tag1 = new byte[16];
+		byte[] tag2 = new byte[16];
+		for (int i = 0; i < data.Length; i ++) {
+			RNG.GetBytes(key);
+			RNG.GetBytes(iv);
+			RNG.GetBytes(data, 0, i);
+			RNG.GetBytes(aad, 0, i);
+
+			Array.Copy(data, 0, tmp, 0, i);
+			for (int j = i; j < tmp.Length; j ++) {
+				tmp[j] = 0xFF;
+			}
+			pp.ChaCha.SetKey(key);
+			pp.Run(iv, tmp, 0, i, aad, 0, i, tag1, true);
+
+			for (int j = i; j < tmp.Length; j ++) {
+				tmp[j] = 0x00;
+			}
+			ppref.ChaCha.SetKey(key);
+			ppref.Run(iv, tmp, 0, i, aad, 0, i, tag2, false);
+
+			CheckEq(data, 0, tmp, 0, i, "cross enc/dec");
+			CheckEq(tag1, tag2, "cross MAC");
+			Console.Write(".");
+		}
+
+		Console.WriteLine("done.");
+	}
+
+	static void TestGHASH()
+	{
+		Console.Write("Testing GHASH... ");
+
+		for (int i = 0; i < KAT_GHASH.Length; i += 4) {
+			byte[] h = ToBin(KAT_GHASH[i]);
+			byte[] a = ToBin(KAT_GHASH[i + 1]);
+			byte[] c = ToBin(KAT_GHASH[i + 2]);
+			byte[] r = ToBin(KAT_GHASH[i + 3]);
+			byte[] y = new byte[16];
+			byte[] p = new byte[16];
+			GHASH.Run(y, h, a);
+			GHASH.Run(y, h, c);
+			uint alen = (uint)a.Length << 3;
+			uint clen = (uint)c.Length << 3;
+			p[ 4] = (byte)(alen >> 24);
+			p[ 5] = (byte)(alen >> 16);
+			p[ 6] = (byte)(alen >>  8);
+			p[ 7] = (byte)alen;
+			p[12] = (byte)(clen >> 24);
+			p[13] = (byte)(clen >> 16);
+			p[14] = (byte)(clen >>  8);
+			p[15] = (byte)clen;
+			GHASH.Run(y, h, p);
+			CheckEq(y, r, "KAT " + (i / 4 + 1));
+		}
+
+		Console.WriteLine("done.");
+	}
+
+	static void TestRSA()
+	{
+		Console.Write("Testing RSA... ");
+
+		DoRSASelfTests(1024);
+		DoRSASelfTests(1231);
+		DoRSASelfTests(2048);
+
+		Console.WriteLine("done.");
+	}
+
+	static void PrintInt(string name, byte[] x)
+	{
+		Console.Write("{0} = 0x", name);
+		foreach (byte b in x) {
+			Console.Write("{0:X2}", b);
+		}
+		Console.WriteLine();
+	}
+
+	static ZInt MakePrime(int size)
+	{
+		ZInt m = ((ZInt)3 << (size - 2)) + (ZInt)3;
+		for (;;) {
+			ZInt r = ZInt.MakeRand(size - 4);
+			ZInt p = (r << 2) + m;
+			if (p % (ZInt)65537 != 1 && p.IsPrime) {
+				return p;
+			}
+		}
+	}
+
+	static ZInt ExtendedGCD(ZInt a, ZInt b, out ZInt u, out ZInt v)
+	{
+		ZInt s = 0, sp = 1;
+		ZInt t = 1, tp = 0;
+		while (b != 0) {
+			ZInt q = a / b;
+			ZInt bn = a - q * b;
+			ZInt sn = sp - q * s;
+			ZInt tn = tp - q * t;
+			a = b;
+			sp = s;
+			tp = t;
+			b = bn;
+			s = sn;
+			t = tn;
+		}
+		u = sp;
+		v = tp;
+		return a;
+	}
+
+	static ZInt ModInverse(ZInt x, ZInt n)
+	{
+		ZInt u, v;
+		ZInt d = ExtendedGCD(x, n, out u, out v);
+		if (x * u + v * n != d) {
+			throw new Exception("bad extended GCD");
+		}
+		if (d != 1) {
+			throw new Exception("not invertible");
+		}
+		return u.Mod(n);
+	}
+
+	static void DoRSASelfTests(int size)
+	{
+		Console.Write("   key gen ({0}): ", size);
+		long begin = DateTime.UtcNow.Ticks;
+
+		/*
+		 * Use ZInt to generate new RSA key.
+		 */
+		ZInt p = MakePrime(size - (size >> 1));
+		ZInt q;
+		do {
+			q = MakePrime(size >> 1);
+		} while (p == q);
+		if (p < q) {
+			ZInt t = p;
+			p = q;
+			q = t;
+		}
+		ZInt n = p * q;
+		ZInt e = 65537;
+		ZInt d = ModInverse(65537, (p - 1) * (q - 1));
+		ZInt dp = d % (p - 1);
+		ZInt dq = d % (q - 1);
+		ZInt iq = ModInverse(q, p);
+
+		long end = DateTime.UtcNow.Ticks;
+		Console.WriteLine("done in {0} ms.", (end - begin) / 10000);
+
+		/*
+		Console.WriteLine("n  = {0}", n);
+		Console.WriteLine("e  = {0}", e);
+		Console.WriteLine("d  = {0}", d);
+		Console.WriteLine("p  = {0}", p);
+		Console.WriteLine("q  = {0}", q);
+		Console.WriteLine("dp = {0}", dp);
+		Console.WriteLine("dq = {0}", dq);
+		Console.WriteLine("iq = {0}", iq);
+		*/
+
+		RSAPrivateKey sk = new RSAPrivateKey(
+			n.ToBytesUnsignedBE(),
+			e.ToBytesUnsignedBE(),
+			d.ToBytesUnsignedBE(),
+			p.ToBytesUnsignedBE(),
+			q.ToBytesUnsignedBE(),
+			dp.ToBytesUnsignedBE(),
+			dq.ToBytesUnsignedBE(),
+			iq.ToBytesUnsignedBE());
+
+		/*
+		Console.Write("   key gen ({0}): ", size);
+		long begin = DateTime.UtcNow.Ticks;
+		RSAPrivateKey sk = RSA.Generate(size);
+		long end = DateTime.UtcNow.Ticks;
+		Console.WriteLine("done in {0} ms.", (end - begin) / 10000);
+		*/
+
+		/*
+		PrintInt("n ", sk.N);
+		PrintInt("e ", sk.E);
+		PrintInt("d ", sk.D);
+		PrintInt("p ", sk.P);
+		PrintInt("q ", sk.Q);
+		PrintInt("dp", sk.DP);
+		PrintInt("dq", sk.DQ);
+		PrintInt("iq", sk.IQ);
+		*/
+
+		RSAPublicKey pk = (RSAPublicKey)sk.PublicKey;
+		for (int i = 0; i < ((size + 7) >> 3) - 11; i ++) {
+			byte[] msg = new byte[i];
+			RNG.GetBytes(msg);
+			IDigest h = new SHA256();
+			h.Update(msg);
+			byte[] hv = h.DoFinal();
+			byte[] sig = RSA.Sign(sk, RSA.PKCS1_SHA256, hv);
+			if (!RSA.Verify(pk, RSA.PKCS1_SHA256, null, hv, sig)) {
+				throw new Exception(String.Format(
+					"RSA sign/verify 1 (len = {0})", i));
+			}
+			if (!RSA.Verify(pk, RSA.PKCS1_SHA256_ALT,
+				RSA.PKCS1_SHA256, hv, sig))
+			{
+				throw new Exception(String.Format(
+					"RSA sign/verify 2 (len = {0})", i));
+			}
+			if (RSA.Verify(pk, RSA.PKCS1_SHA1, null, hv, sig)) {
+				throw new Exception(String.Format(
+					"RSA sign/verify 3 (len = {0})", i));
+			}
+			hv[21] ^= 0x01;
+			if (RSA.Verify(pk, RSA.PKCS1_SHA256, null, hv, sig)) {
+				throw new Exception(String.Format(
+					"RSA sign/verify 4 (len = {0})", i));
+			}
+
+			byte[] enc = RSA.Encrypt(pk, msg);
+			byte[] dec = RSA.Decrypt(sk, enc);
+			if (!Eq(msg, dec)) {
+				throw new Exception(String.Format(
+					"RSA encrypt/decrypt (len = {0})", i));
+			}
+		}
+	}
+
+	static void TestECDSA()
+	{
+		Console.WriteLine("Testing ECDSA... ");
+
+		DoKATECDSA(NIST.P256, ECDSA_K_P256, ECDSA_SIGS_P256);
+		DoKATECDSA(NIST.P384, ECDSA_K_P384, ECDSA_SIGS_P384);
+		DoKATECDSA(NIST.P521, ECDSA_K_P521, ECDSA_SIGS_P521);
+		DoECDSASelfTests(NIST.P256);
+		DoECDSASelfTests(NIST.P384);
+		DoECDSASelfTests(NIST.P521);
+
+		Console.WriteLine("done.");
+	}
+
+	static void DoKATECDSA(ECCurve curve, string[] ks, string[] sigs)
+	{
+		Console.Write("   KAT ({0}): ", curve.Name);
+		ECPublicKey pk = new ECPublicKey(curve, ToBin(ks[0]));
+		pk.CheckValid();
+		Console.Write("<valid pub> ");
+		ECPrivateKey sk = new ECPrivateKey(curve, ToBin(ks[1]));
+		sk.CheckValid();
+		Console.Write("<valid priv> ");
+		IPublicKey pk2 = sk.PublicKey;
+		if (!pk.Equals(pk2) || !pk2.Equals(pk)) {
+			throw new Exception("ECDSA mismatch public/private");
+		}
+		if (!pk.Equals(pk2) || !pk2.Equals(pk)) {
+			throw new Exception("ECDSA mismatch public/private");
+		}
+		for (int i = 0; i < 10; i ++) {
+			byte[] r = ToBin(sigs[i << 1]);
+			byte[] s = ToBin(sigs[(i << 1) + 1]);
+			byte[] msg = Encoding.UTF8.GetBytes(
+				(i < 5) ? "sample" : "test");
+			IDigest dig;
+			switch (i % 5) {
+			case 0:  dig = new SHA1(); break;
+			case 1:  dig = new SHA224(); break;
+			case 2:  dig = new SHA256(); break;
+			case 3:  dig = new SHA384(); break;
+			default: dig = new SHA512(); break;
+			}
+			dig.Update(msg);
+			byte[] hv = dig.DoFinal();
+			DoKATECDSA(pk, sk, dig, r, s, hv);
+			Console.Write(".");
+		}
+		Console.WriteLine();
+	}
+
+	static void DoKATECDSA(ECPublicKey pk, ECPrivateKey sk,
+		IDigest dig, byte[] r, byte[] s, byte[] hv)
+	{
+		if (r.Length != s.Length) {
+			throw new ArgumentException();
+		}
+		byte[] sig1 = new byte[r.Length + s.Length];
+		Array.Copy(r, 0, sig1, 0, r.Length);
+		Array.Copy(s, 0, sig1, r.Length, s.Length);
+		byte[] sig2 = ECDSA.SigRawToAsn1(sig1);
+		byte[] sig3 = ECDSA.SigRawToAsn1(ECDSA.SigAsn1ToRaw(sig2));
+		if (!Eq(sig2, sig3)) {
+			throw new Exception("ECDSA sig enc/dec");
+		}
+
+		if (!ECDSA.VerifyRaw(pk, hv, sig1)) {
+			throw new Exception("ECDSA verify 1");
+		}
+		if (!ECDSA.Verify(pk, hv, sig2)) {
+			throw new Exception("ECDSA verify 2");
+		}
+		byte[] hv2 = new byte[hv.Length + 2];
+		Array.Copy(hv, 0, hv2, 1, hv.Length);
+		if (!ECDSA.VerifyRaw(pk, hv2, 1, hv.Length, sig1)) {
+			throw new Exception("ECDSA verify 3");
+		}
+		hv2[1] ^= (byte)0x02;
+		if (ECDSA.VerifyRaw(pk, hv2, 1, hv.Length, sig1)) {
+			throw new Exception("ECDSA verify 4");
+		}
+
+		byte[] sig4 = ECDSA.SignRaw(sk, null, hv);
+		if (Eq(sig1, sig4)) {
+			throw new Exception("ECDSA sig randomized");
+		}
+		if (!ECDSA.VerifyRaw(pk, hv, sig4)) {
+			throw new Exception("ECDSA verify 5");
+		}
+		byte[] sig5 = ECDSA.SignRaw(sk, dig, hv);
+		if (!Eq(sig1, sig5)) {
+			throw new Exception("ECDSA sig deterministic");
+		}
+	}
+
+	static void DoECDSASelfTests(ECCurve curve)
+	{
+		Console.Write("   self ({0}): ", curve.Name);
+		ECPrivateKey sk = ECDSA.Generate(curve);
+		sk.CheckValid();
+		Console.Write("<valid> ");
+		ECPublicKey pk = sk.PublicKey;
+		IDigest h = new SHA256();
+		byte[] msg = new byte[32];
+		for (int i = 0; i < 10; i ++) {
+			RNG.GetBytes(msg);
+			byte[] hv = h.Hash(msg);
+			byte[] sig = ECDSA.Sign(sk, null, hv);
+			if (!ECDSA.Verify(pk, hv, sig)) {
+				throw new Exception("ECDSA sign/verify");
+			}
+			Console.Write(".");
+		}
+		Console.WriteLine();
+	}
+
+	static void DoKATHash(IDigest h, byte[] data, byte[] refOut)
+	{
+		h.Update(data);
+		CheckEq(h.DoFinal(), refOut, "KAT 1");
+		h.Update(data, 0, data.Length);
+		byte[] tmp = new byte[h.DigestSize];
+		h.DoFinal(tmp, 0);
+		CheckEq(tmp, refOut, "KAT 2");
+		foreach (byte b in data) {
+			h.Update(b);
+		}
+		CheckEq(h.DoFinal(), refOut, "KAT 3");
+		for (int t = 0; t < data.Length; t ++) {
+			h.Update(data, 0, t);
+			h.Update(data, t, data.Length - t);
+			h.DoFinal(tmp, 0);
+			CheckEq(tmp, refOut, "KAT 4." + t);
+		}
+		foreach (byte b in data) {
+			h.Update(b);
+			h.DoPartial(tmp, 0);
+		}
+		CheckEq(tmp, refOut, "KAT 5");
+		h.Reset();
+		h.Update(data);
+		CheckEq(h.DoFinal(), refOut, "KAT 6");
+		for (int t = 0; t < data.Length; t ++) {
+			h.Update(data, 0, t);
+			IDigest h2 = h.Dup();
+			h.Update(data, t, data.Length - t);
+			h.DoFinal(tmp, 0);
+			CheckEq(tmp, refOut, "KAT 7." + t);
+			h2.Update(data, t, data.Length - t);
+			h2.DoFinal(tmp, 0);
+			CheckEq(tmp, refOut, "KAT 8." + t);
+		}
+	}
+
+	static void DoKATHashLong(IDigest h, string refOut)
+	{
+		byte[] buf = new byte[1000];
+		for (int i = 0; i < buf.Length; i ++) {
+			buf[i] = (byte)'a';
+		}
+		for (int i = 0; i < 1000; i ++) {
+			h.Update(buf);
+		}
+		CheckEq(h.DoFinal(), ToBin(refOut), "KAT Long");
+	}
+
+	static void DoKATHMAC(HMAC hm, byte[] key, byte[] data, byte[] refOut)
+	{
+		hm.SetKey(key);
+		hm.Update(data);
+		CheckEq(hm.DoFinal(), refOut, "KAT 1");
+		hm.Update(data, 0, data.Length);
+		byte[] tmp = new byte[hm.MACSize];
+		hm.DoFinal(tmp, 0);
+		CheckEq(tmp, refOut, "KAT 2");
+		foreach (byte b in data) {
+			hm.Update(b);
+		}
+		CheckEq(hm.DoFinal(), refOut, "KAT 3");
+		for (int t = 0; t < data.Length; t ++) {
+			hm.Update(data, 0, t);
+			hm.Update(data, t, data.Length - t);
+			hm.DoFinal(tmp, 0);
+			CheckEq(tmp, refOut, "KAT 4." + t);
+		}
+		for (int t = 0; t < data.Length; t ++) {
+			hm.Update(data, 0, t);
+			HMAC hm2 = hm.Dup();
+			hm.Update(data, t, data.Length - t);
+			hm.DoFinal(tmp, 0);
+			CheckEq(tmp, refOut, "KAT 5." + t);
+			hm2.Update(data, t, data.Length - t);
+			hm2.DoFinal(tmp, 0);
+			CheckEq(tmp, refOut, "KAT 6." + t);
+		}
+	}
+
+	static void DoKATBlockCipherRaw(IBlockCipher bc, string[] kat)
+	{
+		for (int i = 0; i < kat.Length; i += 3) {
+			byte[] key = ToBin(kat[i]);
+			byte[] plain = ToBin(kat[i + 1]);
+			byte[] cipher = ToBin(kat[i + 2]);
+			int blen = bc.BlockSize;
+			if (blen != plain.Length || blen != cipher.Length) {
+				throw new Exception(string.Format(
+					"block size mismatch: {0} / {1},{2}",
+					blen, plain.Length, cipher.Length));
+			}
+			bc.SetKey(key);
+			byte[] tmp = new byte[blen];
+			Array.Copy(plain, 0, tmp, 0, blen);
+			bc.BlockEncrypt(tmp, 0);
+			CheckEq(tmp, cipher, "KAT encrypt");
+			bc.BlockDecrypt(tmp, 0);
+			CheckEq(tmp, plain, "KAT decrypt");
+		}
+	}
+
+	static void DoKATBlockCipherCBC(IBlockCipher bc, string[] kat)
+	{
+		for (int i = 0; i < kat.Length; i += 4) {
+			byte[] key = ToBin(kat[i]);
+			byte[] iv = ToBin(kat[i + 1]);
+			byte[] plain = ToBin(kat[i + 2]);
+			byte[] cipher = ToBin(kat[i + 3]);
+			int blen = bc.BlockSize;
+			if (blen != iv.Length
+				|| (plain.Length % blen) != 0
+				|| (cipher.Length % blen) != 0
+				|| plain.Length != cipher.Length)
+			{
+				throw new Exception(string.Format(
+					"block size mismatch:"
+					+ " {0} / {1},{2},{3}",
+					blen, iv.Length,
+					plain.Length, cipher.Length));
+			}
+			bc.SetKey(key);
+			byte[] tmp = new byte[plain.Length];
+			Array.Copy(plain, 0, tmp, 0, tmp.Length);
+			bc.CBCEncrypt(iv, tmp);
+			CheckEq(tmp, cipher, "KAT CBC encrypt (1)");
+			bc.CBCDecrypt(iv, tmp);
+			CheckEq(tmp, plain, "KAT CBC decrypt (1)");
+
+			byte[] iv2 = new byte[blen];
+			Array.Copy(iv, 0, iv2, 0, blen);
+			for (int j = 0; j < tmp.Length; j += blen) {
+				bc.CBCEncrypt(iv2, tmp, j, blen);
+				Array.Copy(tmp, j, iv2, 0, blen);
+			}
+			CheckEq(tmp, cipher, "KAT CBC encrypt (2)");
+			byte[] iv3 = new byte[blen];
+			Array.Copy(iv, 0, iv2, 0, blen);
+			for (int j = 0; j < tmp.Length; j += blen) {
+				Array.Copy(tmp, j, iv3, 0, blen);
+				bc.CBCDecrypt(iv2, tmp, j, blen);
+				Array.Copy(iv3, 0, iv2, 0, blen);
+			}
+			CheckEq(tmp, plain, "KAT CBC decrypt (2)");
+		}
+	}
+
+	static void DoKATBlockCipherCTR(IBlockCipher bc, string[] kat)
+	{
+		for (int i = 0; i < kat.Length; i += 4) {
+			byte[] key = ToBin(kat[i]);
+			byte[] iv = ToBin(kat[i + 1]);
+			byte[] plain = ToBin(kat[i + 2]);
+			byte[] cipher = ToBin(kat[i + 3]);
+			int blen = bc.BlockSize;
+			if (blen != (iv.Length + 4)
+				|| plain.Length != cipher.Length)
+			{
+				throw new Exception(string.Format(
+					"block size mismatch:"
+					+ " {0} / {1},{2},{3}",
+					blen, iv.Length,
+					plain.Length, cipher.Length));
+			}
+			bc.SetKey(key);
+			byte[] tmp = new byte[plain.Length];
+			Array.Copy(plain, 0, tmp, 0, tmp.Length);
+			uint cc;
+			cc = bc.CTRRun(iv, 1, tmp);
+			CheckEq(tmp, cipher, "KAT CTR encrypt (1)");
+			if (cc != 1 + ((tmp.Length + blen - 1) / blen)) {
+				throw new Exception(string.Format(
+					"wrong CTR counter: {0} / {1}",
+					cc, tmp.Length));
+			}
+			cc = bc.CTRRun(iv, 1, tmp);
+			CheckEq(tmp, plain, "KAT CTR decrypt (1)");
+			if (cc != 1 + ((tmp.Length + blen - 1) / blen)) {
+				throw new Exception(string.Format(
+					"wrong CTR counter: {0} / {1}",
+					cc, tmp.Length));
+			}
+
+			cc = 1;
+			for (int j = 0; j < tmp.Length; j += blen) {
+				int clen = Math.Min(blen, tmp.Length - j);
+				uint cc2 = bc.CTRRun(iv, cc, tmp, j, clen);
+				if (cc2 != cc + 1) {
+					throw new Exception(
+						"wrong CTR counter update");
+				}
+				cc = cc2;
+			}
+			CheckEq(tmp, cipher, "KAT CTR encrypt (2)");
+		}
+	}
+
+	static void DoMonteCarloAESEncrypt(IBlockCipher bc,
+		string skey, string splain, string scipher)
+	{
+		byte[] key = ToBin(skey);
+		byte[] buf = ToBin(splain);
+		byte[] pbuf = new byte[buf.Length];
+		byte[] cipher = ToBin(scipher);
+		for (int i = 0; i < 100; i ++) {
+			bc.SetKey(key);
+			for (int j = 0; j < 1000; j ++) {
+				Array.Copy(buf, 0, pbuf, 0, pbuf.Length);
+				bc.BlockEncrypt(buf);
+			}
+			switch (key.Length) {
+			case 16:
+				for (int k = 0; k < 16; k ++) {
+					key[k] ^= buf[k];
+				}
+				break;
+			case 24:
+				for (int k = 0; k < 8; k ++) {
+					key[k] ^= pbuf[8 + k];
+				}
+				for (int k = 0; k < 16; k ++) {
+					key[8 + k] ^= buf[k];
+				}
+				break;
+			case 32:
+				for (int k = 0; k < 16; k ++) {
+					key[k] ^= pbuf[k];
+				}
+				for (int k = 0; k < 16; k ++) {
+					key[16 + k] ^= buf[k];
+				}
+				break;
+			}
+			Console.Write(".");
+		}
+		Console.Write(" ");
+		CheckEq(buf, cipher, "MC AES encrypt");
+	}
+
+	static void DoMonteCarloAESDecrypt(IBlockCipher bc,
+		string skey, string splain, string scipher)
+	{
+		byte[] key = ToBin(skey);
+		byte[] buf = ToBin(splain);
+		byte[] pbuf = new byte[buf.Length];
+		byte[] cipher = ToBin(scipher);
+		for (int i = 0; i < 100; i ++) {
+			bc.SetKey(key);
+			for (int j = 0; j < 1000; j ++) {
+				Array.Copy(buf, 0, pbuf, 0, pbuf.Length);
+				bc.BlockDecrypt(buf);
+			}
+			switch (key.Length) {
+			case 16:
+				for (int k = 0; k < 16; k ++) {
+					key[k] ^= buf[k];
+				}
+				break;
+			case 24:
+				for (int k = 0; k < 8; k ++) {
+					key[k] ^= pbuf[8 + k];
+				}
+				for (int k = 0; k < 16; k ++) {
+					key[8 + k] ^= buf[k];
+				}
+				break;
+			case 32:
+				for (int k = 0; k < 16; k ++) {
+					key[k] ^= pbuf[k];
+				}
+				for (int k = 0; k < 16; k ++) {
+					key[16 + k] ^= buf[k];
+				}
+				break;
+			}
+			Console.Write(".");
+		}
+		Console.Write(" ");
+		CheckEq(buf, cipher, "MC AES decrypt");
+	}
+
+	static void DoMonteCarloDESEncrypt(IBlockCipher bc)
+	{
+		byte[] k1 = ToBin("9ec2372c86379df4");
+		byte[] k2 = ToBin("ad7ac4464f73805d");
+		byte[] k3 = ToBin("20c4f87564527c91");
+		byte[] buf = ToBin("b624d6bd41783ab1");
+		byte[] cipher = ToBin("eafd97b190b167fe");
+		byte[] key = new byte[24];
+		for (int i = 0; i < 400; i ++) {
+			Array.Copy(k1, 0, key, 0, 8);
+			Array.Copy(k2, 0, key, 8, 8);
+			Array.Copy(k3, 0, key, 16, 8);
+			bc.SetKey(key);
+			for (int j = 0; j < 10000; j ++) {
+				bc.BlockEncrypt(buf, 0);
+				switch (j) {
+				case 9997:
+					for (int n = 0; n < 8; n ++) {
+						k3[n] ^= buf[n];
+					}
+					break;
+				case 9998:
+					for (int n = 0; n < 8; n ++) {
+						k2[n] ^= buf[n];
+					}
+					break;
+				case 9999:
+					for (int n = 0; n < 8; n ++) {
+						k1[n] ^= buf[n];
+					}
+					break;
+				}
+			}
+			Console.Write(".");
+		}
+		Console.Write(" ");
+		CheckEq(buf, cipher, "MC DES encrypt");
+	}
+
+	static void DoMonteCarloDESDecrypt(IBlockCipher bc)
+	{
+		byte[] k1 = ToBin("79b63486e0ce37e0");
+		byte[] k2 = ToBin("08e65231abae3710");
+		byte[] k3 = ToBin("1f5eb69e925ef185");
+		byte[] buf = ToBin("2783aa729432fe96");
+		byte[] cipher = ToBin("44937ca532cdbf98");
+		byte[] key = new byte[24];
+		for (int i = 0; i < 400; i ++) {
+			Array.Copy(k1, 0, key, 0, 8);
+			Array.Copy(k2, 0, key, 8, 8);
+			Array.Copy(k3, 0, key, 16, 8);
+			bc.SetKey(key);
+			for (int j = 0; j < 10000; j ++) {
+				bc.BlockDecrypt(buf, 0);
+				switch (j) {
+				case 9997:
+					for (int n = 0; n < 8; n ++) {
+						k3[n] ^= buf[n];
+					}
+					break;
+				case 9998:
+					for (int n = 0; n < 8; n ++) {
+						k2[n] ^= buf[n];
+					}
+					break;
+				case 9999:
+					for (int n = 0; n < 8; n ++) {
+						k1[n] ^= buf[n];
+					}
+					break;
+				}
+			}
+			Console.Write(".");
+		}
+		Console.Write(" ");
+		CheckEq(buf, cipher, "MC DES decrypt");
+	}
+
+	static bool Eq(byte[] a1, byte[] a2)
+	{
+		if (a1 == a2) {
+			return true;
+		}
+		if (a1 == null || a2 == null) {
+			return false;
+		}
+		int n = a1.Length;
+		if (n != a2.Length) {
+			return false;
+		}
+		for (int i = 0; i < n; i ++) {
+			if (a1[i] != a2[i]) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	static bool Eq(byte[] a1, int off1, byte[] a2, int off2, int len)
+	{
+		for (int i = 0; i < len; i ++) {
+			if (a1[off1 + i] != a2[off1 + i]) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	static void CheckEq(byte[] a1, byte[] a2, string msg)
+	{
+		if (Eq(a1, a2)) {
+			return;
+		}
+		throw new Exception(string.Format(
+			"Not equal ({0}):\nv1 = {1}\nv2 = {2}",
+			msg, ToHex(a1), ToHex(a2)));
+	}
+
+	static void CheckEq(byte[] a1, int off1,
+		byte[] a2, int off2, int len, string msg)
+	{
+		if (Eq(a1, off1, a2, off2, len)) {
+			return;
+		}
+		throw new Exception(string.Format(
+			"Not equal ({0}):\nv1 = {1}\nv2 = {2}",
+			msg, ToHex(a1, off1, len), ToHex(a2, off2, len)));
+	}
+
+	static byte[] ToBin(string str)
+	{
+		MemoryStream ms = new MemoryStream();
+		bool z = true;
+		int acc = 0;
+		foreach (char c in str) {
+			int d;
+			if (c >= '0' && c <= '9') {
+				d = c - '0';
+			} else if (c >= 'A' && c <= 'F') {
+				d = c - ('A' - 10);
+			} else if (c >= 'a' && c <= 'f') {
+				d = c - ('a' - 10);
+			} else if (c == ' ' || c == '\t' || c == ':') {
+				continue;
+			} else {
+				throw new ArgumentException(String.Format(
+					"not hex: U+{0:X4}", (int)c));
+			}
+			if (z) {
+				acc = d;
+			} else {
+				ms.WriteByte((byte)((acc << 4) + d));
+			}
+			z = !z;
+		}
+		if (!z) {
+			throw new ArgumentException("final half byte");
+		}
+		return ms.ToArray();
+	}
+
+	static string ToHex(byte[] buf)
+	{
+		return ToHex(buf, 0, buf.Length);
+	}
+
+	static string ToHex(byte[] buf, int off, int len)
+	{
+		StringBuilder sb = new StringBuilder();
+		for (int i = 0; i < len; i ++) {
+			sb.AppendFormat("{0:X2}", buf[off + i]);
+		}
+		return sb.ToString();
+	}
+
+	static string[] KAT_MD5 = {
+		"",
+		"d41d8cd98f00b204e9800998ecf8427e",
+		"a",
+		"0cc175b9c0f1b6a831c399e269772661",
+		"abc",
+		"900150983cd24fb0d6963f7d28e17f72",
+		"message digest",
+		"f96b697d7cb7938d525a2f31aaf161d0",
+		"abcdefghijklmnopqrstuvwxyz",
+		"c3fcd3d76192e4007dfb496cca67e13b",
+		"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
+		"d174ab98d277d9f5a5611c2c9f419d9f",
+		"12345678901234567890123456789012345678901234567890123456789012345678901234567890",
+		"57edf4a22be3c955ac49da2e2107b67a"
+	};
+
+	static string[] KAT_SHA1 = {
+		"abc",
+		"a9993e364706816aba3e25717850c26c9cd0d89d",
+		"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+		"84983e441c3bd26ebaae4aa1f95129e5e54670f1"
+	};
+
+	static string[] KAT_SHA224 = {
+		"abc",
+		"23097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7",
+		"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+		"75388b16512776cc5dba5da1fd890150b0c6455cb4f58b1952522525"
+	};
+
+	static string[] KAT_SHA256 = {
+		"abc",
+		"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
+		"abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq",
+		"248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1"
+	};
+
+	static string[] KAT_SHA384 = {
+		"abc",
+		"cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7",
+		"abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu",
+		"09330c33f71147e83d192fc782cd1b4753111b173b3b05d22fa08086e3b0f712fcc7c71a557e2db966c3e9fa91746039"
+	};
+
+	static string[] KAT_SHA512 = {
+		"abc",
+		"ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f",
+		"abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu",
+		"8e959b75dae313da8cf4f72814fc143f8f7779c6eb9f7fa17299aeadb6889018501d289e4900f7e4331b99dec4b5433ac7d329eeb6dd26545e96e55b874be909"
+	};
+
+	/*
+	 * AES known-answer tests.
+	 * Order: key, plaintext, ciphertext.
+	 */
+	static string[] KAT_AES_RAW = {
+		/*
+		 * From FIPS-197.
+		 */
+		"000102030405060708090a0b0c0d0e0f",
+		"00112233445566778899aabbccddeeff",
+		"69c4e0d86a7b0430d8cdb78070b4c55a",
+
+		"000102030405060708090a0b0c0d0e0f1011121314151617",
+		"00112233445566778899aabbccddeeff",
+		"dda97ca4864cdfe06eaf70a0ec0d7191",
+
+		"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
+		"00112233445566778899aabbccddeeff",
+		"8ea2b7ca516745bfeafc49904b496089",
+
+		/*
+		 * From NIST validation suite (ECBVarTxt128.rsp).
+		 */
+		"00000000000000000000000000000000",
+		"80000000000000000000000000000000",
+		"3ad78e726c1ec02b7ebfe92b23d9ec34",
+
+		"00000000000000000000000000000000",
+		"c0000000000000000000000000000000",
+		"aae5939c8efdf2f04e60b9fe7117b2c2",
+
+		"00000000000000000000000000000000",
+		"e0000000000000000000000000000000",
+		"f031d4d74f5dcbf39daaf8ca3af6e527",
+
+		"00000000000000000000000000000000",
+		"f0000000000000000000000000000000",
+		"96d9fd5cc4f07441727df0f33e401a36",
+
+		"00000000000000000000000000000000",
+		"f8000000000000000000000000000000",
+		"30ccdb044646d7e1f3ccea3dca08b8c0",
+
+		"00000000000000000000000000000000",
+		"fc000000000000000000000000000000",
+		"16ae4ce5042a67ee8e177b7c587ecc82",
+
+		"00000000000000000000000000000000",
+		"fe000000000000000000000000000000",
+		"b6da0bb11a23855d9c5cb1b4c6412e0a",
+
+		"00000000000000000000000000000000",
+		"ff000000000000000000000000000000",
+		"db4f1aa530967d6732ce4715eb0ee24b",
+
+		"00000000000000000000000000000000",
+		"ff800000000000000000000000000000",
+		"a81738252621dd180a34f3455b4baa2f",
+
+		"00000000000000000000000000000000",
+		"ffc00000000000000000000000000000",
+		"77e2b508db7fd89234caf7939ee5621a",
+
+		"00000000000000000000000000000000",
+		"ffe00000000000000000000000000000",
+		"b8499c251f8442ee13f0933b688fcd19",
+
+		"00000000000000000000000000000000",
+		"fff00000000000000000000000000000",
+		"965135f8a81f25c9d630b17502f68e53",
+
+		"00000000000000000000000000000000",
+		"fff80000000000000000000000000000",
+		"8b87145a01ad1c6cede995ea3670454f",
+
+		"00000000000000000000000000000000",
+		"fffc0000000000000000000000000000",
+		"8eae3b10a0c8ca6d1d3b0fa61e56b0b2",
+
+		"00000000000000000000000000000000",
+		"fffe0000000000000000000000000000",
+		"64b4d629810fda6bafdf08f3b0d8d2c5",
+
+		"00000000000000000000000000000000",
+		"ffff0000000000000000000000000000",
+		"d7e5dbd3324595f8fdc7d7c571da6c2a",
+
+		"00000000000000000000000000000000",
+		"ffff8000000000000000000000000000",
+		"f3f72375264e167fca9de2c1527d9606",
+
+		"00000000000000000000000000000000",
+		"ffffc000000000000000000000000000",
+		"8ee79dd4f401ff9b7ea945d86666c13b",
+
+		"00000000000000000000000000000000",
+		"ffffe000000000000000000000000000",
+		"dd35cea2799940b40db3f819cb94c08b",
+
+		"00000000000000000000000000000000",
+		"fffff000000000000000000000000000",
+		"6941cb6b3e08c2b7afa581ebdd607b87",
+
+		"00000000000000000000000000000000",
+		"fffff800000000000000000000000000",
+		"2c20f439f6bb097b29b8bd6d99aad799",
+
+		"00000000000000000000000000000000",
+		"fffffc00000000000000000000000000",
+		"625d01f058e565f77ae86378bd2c49b3",
+
+		"00000000000000000000000000000000",
+		"fffffe00000000000000000000000000",
+		"c0b5fd98190ef45fbb4301438d095950",
+
+		"00000000000000000000000000000000",
+		"ffffff00000000000000000000000000",
+		"13001ff5d99806efd25da34f56be854b",
+
+		"00000000000000000000000000000000",
+		"ffffff80000000000000000000000000",
+		"3b594c60f5c8277a5113677f94208d82",
+
+		"00000000000000000000000000000000",
+		"ffffffc0000000000000000000000000",
+		"e9c0fc1818e4aa46bd2e39d638f89e05",
+
+		"00000000000000000000000000000000",
+		"ffffffe0000000000000000000000000",
+		"f8023ee9c3fdc45a019b4e985c7e1a54",
+
+		"00000000000000000000000000000000",
+		"fffffff0000000000000000000000000",
+		"35f40182ab4662f3023baec1ee796b57",
+
+		"00000000000000000000000000000000",
+		"fffffff8000000000000000000000000",
+		"3aebbad7303649b4194a6945c6cc3694",
+
+		"00000000000000000000000000000000",
+		"fffffffc000000000000000000000000",
+		"a2124bea53ec2834279bed7f7eb0f938",
+
+		"00000000000000000000000000000000",
+		"fffffffe000000000000000000000000",
+		"b9fb4399fa4facc7309e14ec98360b0a",
+
+		"00000000000000000000000000000000",
+		"ffffffff000000000000000000000000",
+		"c26277437420c5d634f715aea81a9132",
+
+		"00000000000000000000000000000000",
+		"ffffffff800000000000000000000000",
+		"171a0e1b2dd424f0e089af2c4c10f32f",
+
+		"00000000000000000000000000000000",
+		"ffffffffc00000000000000000000000",
+		"7cadbe402d1b208fe735edce00aee7ce",
+
+		"00000000000000000000000000000000",
+		"ffffffffe00000000000000000000000",
+		"43b02ff929a1485af6f5c6d6558baa0f",
+
+		"00000000000000000000000000000000",
+		"fffffffff00000000000000000000000",
+		"092faacc9bf43508bf8fa8613ca75dea",
+
+		"00000000000000000000000000000000",
+		"fffffffff80000000000000000000000",
+		"cb2bf8280f3f9742c7ed513fe802629c",
+
+		"00000000000000000000000000000000",
+		"fffffffffc0000000000000000000000",
+		"215a41ee442fa992a6e323986ded3f68",
+
+		"00000000000000000000000000000000",
+		"fffffffffe0000000000000000000000",
+		"f21e99cf4f0f77cea836e11a2fe75fb1",
+
+		"00000000000000000000000000000000",
+		"ffffffffff0000000000000000000000",
+		"95e3a0ca9079e646331df8b4e70d2cd6",
+
+		"00000000000000000000000000000000",
+		"ffffffffff8000000000000000000000",
+		"4afe7f120ce7613f74fc12a01a828073",
+
+		"00000000000000000000000000000000",
+		"ffffffffffc000000000000000000000",
+		"827f000e75e2c8b9d479beed913fe678",
+
+		"00000000000000000000000000000000",
+		"ffffffffffe000000000000000000000",
+		"35830c8e7aaefe2d30310ef381cbf691",
+
+		"00000000000000000000000000000000",
+		"fffffffffff000000000000000000000",
+		"191aa0f2c8570144f38657ea4085ebe5",
+
+		"00000000000000000000000000000000",
+		"fffffffffff800000000000000000000",
+		"85062c2c909f15d9269b6c18ce99c4f0",
+
+		"00000000000000000000000000000000",
+		"fffffffffffc00000000000000000000",
+		"678034dc9e41b5a560ed239eeab1bc78",
+
+		"00000000000000000000000000000000",
+		"fffffffffffe00000000000000000000",
+		"c2f93a4ce5ab6d5d56f1b93cf19911c1",
+
+		"00000000000000000000000000000000",
+		"ffffffffffff00000000000000000000",
+		"1c3112bcb0c1dcc749d799743691bf82",
+
+		"00000000000000000000000000000000",
+		"ffffffffffff80000000000000000000",
+		"00c55bd75c7f9c881989d3ec1911c0d4",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffc0000000000000000000",
+		"ea2e6b5ef182b7dff3629abd6a12045f",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffe0000000000000000000",
+		"22322327e01780b17397f24087f8cc6f",
+
+		"00000000000000000000000000000000",
+		"fffffffffffff0000000000000000000",
+		"c9cacb5cd11692c373b2411768149ee7",
+
+		"00000000000000000000000000000000",
+		"fffffffffffff8000000000000000000",
+		"a18e3dbbca577860dab6b80da3139256",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffc000000000000000000",
+		"79b61c37bf328ecca8d743265a3d425c",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffe000000000000000000",
+		"d2d99c6bcc1f06fda8e27e8ae3f1ccc7",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffff000000000000000000",
+		"1bfd4b91c701fd6b61b7f997829d663b",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffff800000000000000000",
+		"11005d52f25f16bdc9545a876a63490a",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffc00000000000000000",
+		"3a4d354f02bb5a5e47d39666867f246a",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffe00000000000000000",
+		"d451b8d6e1e1a0ebb155fbbf6e7b7dc3",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffff00000000000000000",
+		"6898d4f42fa7ba6a10ac05e87b9f2080",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffff80000000000000000",
+		"b611295e739ca7d9b50f8e4c0e754a3f",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffc0000000000000000",
+		"7d33fc7d8abe3ca1936759f8f5deaf20",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffe0000000000000000",
+		"3b5e0f566dc96c298f0c12637539b25c",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffff0000000000000000",
+		"f807c3e7985fe0f5a50e2cdb25c5109e",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffff8000000000000000",
+		"41f992a856fb278b389a62f5d274d7e9",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffc000000000000000",
+		"10d3ed7a6fe15ab4d91acbc7d0767ab1",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffe000000000000000",
+		"21feecd45b2e675973ac33bf0c5424fc",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffff000000000000000",
+		"1480cb3955ba62d09eea668f7c708817",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffff800000000000000",
+		"66404033d6b72b609354d5496e7eb511",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffc00000000000000",
+		"1c317a220a7d700da2b1e075b00266e1",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffe00000000000000",
+		"ab3b89542233f1271bf8fd0c0f403545",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffff00000000000000",
+		"d93eae966fac46dca927d6b114fa3f9e",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffff80000000000000",
+		"1bdec521316503d9d5ee65df3ea94ddf",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffc0000000000000",
+		"eef456431dea8b4acf83bdae3717f75f",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffe0000000000000",
+		"06f2519a2fafaa596bfef5cfa15c21b9",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffff0000000000000",
+		"251a7eac7e2fe809e4aa8d0d7012531a",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffff8000000000000",
+		"3bffc16e4c49b268a20f8d96a60b4058",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffc000000000000",
+		"e886f9281999c5bb3b3e8862e2f7c988",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffe000000000000",
+		"563bf90d61beef39f48dd625fcef1361",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffff000000000000",
+		"4d37c850644563c69fd0acd9a049325b",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffff800000000000",
+		"b87c921b91829ef3b13ca541ee1130a6",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffc00000000000",
+		"2e65eb6b6ea383e109accce8326b0393",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffe00000000000",
+		"9ca547f7439edc3e255c0f4d49aa8990",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffff00000000000",
+		"a5e652614c9300f37816b1f9fd0c87f9",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffff80000000000",
+		"14954f0b4697776f44494fe458d814ed",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffc0000000000",
+		"7c8d9ab6c2761723fe42f8bb506cbcf7",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffe0000000000",
+		"db7e1932679fdd99742aab04aa0d5a80",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffff0000000000",
+		"4c6a1c83e568cd10f27c2d73ded19c28",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffff8000000000",
+		"90ecbe6177e674c98de412413f7ac915",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffc000000000",
+		"90684a2ac55fe1ec2b8ebd5622520b73",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffe000000000",
+		"7472f9a7988607ca79707795991035e6",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffff000000000",
+		"56aff089878bf3352f8df172a3ae47d8",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffff800000000",
+		"65c0526cbe40161b8019a2a3171abd23",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffc00000000",
+		"377be0be33b4e3e310b4aabda173f84f",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffe00000000",
+		"9402e9aa6f69de6504da8d20c4fcaa2f",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffff00000000",
+		"123c1f4af313ad8c2ce648b2e71fb6e1",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffff80000000",
+		"1ffc626d30203dcdb0019fb80f726cf4",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffffc0000000",
+		"76da1fbe3a50728c50fd2e621b5ad885",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffffe0000000",
+		"082eb8be35f442fb52668e16a591d1d6",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffff0000000",
+		"e656f9ecf5fe27ec3e4a73d00c282fb3",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffff8000000",
+		"2ca8209d63274cd9a29bb74bcd77683a",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffffc000000",
+		"79bf5dce14bb7dd73a8e3611de7ce026",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffffe000000",
+		"3c849939a5d29399f344c4a0eca8a576",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffffff000000",
+		"ed3c0a94d59bece98835da7aa4f07ca2",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffffff800000",
+		"63919ed4ce10196438b6ad09d99cd795",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffffffc00000",
+		"7678f3a833f19fea95f3c6029e2bc610",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffffffe00000",
+		"3aa426831067d36b92be7c5f81c13c56",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffffff00000",
+		"9272e2d2cdd11050998c845077a30ea0",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffffff80000",
+		"088c4b53f5ec0ff814c19adae7f6246c",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffffffc0000",
+		"4010a5e401fdf0a0354ddbcc0d012b17",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffffffe0000",
+		"a87a385736c0a6189bd6589bd8445a93",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffffffff0000",
+		"545f2b83d9616dccf60fa9830e9cd287",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffffffff8000",
+		"4b706f7f92406352394037a6d4f4688d",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffc000",
+		"b7972b3941c44b90afa7b264bfba7387",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffe000",
+		"6f45732cf10881546f0fd23896d2bb60",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffffffff000",
+		"2e3579ca15af27f64b3c955a5bfc30ba",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffffffff800",
+		"34a2c5a91ae2aec99b7d1b5fa6780447",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffc00",
+		"a4d6616bd04f87335b0e53351227a9ee",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffe00",
+		"7f692b03945867d16179a8cefc83ea3f",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffff00",
+		"3bd141ee84a0e6414a26e7a4f281f8a2",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffff80",
+		"d1788f572d98b2b16ec5d5f3922b99bc",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffffc0",
+		"0833ff6f61d98a57b288e8c3586b85a6",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffffe0",
+		"8568261797de176bf0b43becc6285afb",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffff0",
+		"f9b0fda0c4a898f5b9e6f661c4ce4d07",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffff8",
+		"8ade895913685c67c5269f8aae42983e",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffffc",
+		"39bde67d5c8ed8a8b1c37eb8fa9f5ac0",
+
+		"00000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffffe",
+		"5c005e72c1418c44f569f2ea33ba54f3",
+
+		"00000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffffff",
+		"3f5b8cc9ea855a0afa7347d23e8d664e",
+
+		/*
+		 * From NIST validation suite (ECBVarTxt192.rsp).
+		 */
+		"000000000000000000000000000000000000000000000000",
+		"80000000000000000000000000000000",
+		"6cd02513e8d4dc986b4afe087a60bd0c",
+
+		"000000000000000000000000000000000000000000000000",
+		"c0000000000000000000000000000000",
+		"2ce1f8b7e30627c1c4519eada44bc436",
+
+		"000000000000000000000000000000000000000000000000",
+		"e0000000000000000000000000000000",
+		"9946b5f87af446f5796c1fee63a2da24",
+
+		"000000000000000000000000000000000000000000000000",
+		"f0000000000000000000000000000000",
+		"2a560364ce529efc21788779568d5555",
+
+		"000000000000000000000000000000000000000000000000",
+		"f8000000000000000000000000000000",
+		"35c1471837af446153bce55d5ba72a0a",
+
+		"000000000000000000000000000000000000000000000000",
+		"fc000000000000000000000000000000",
+		"ce60bc52386234f158f84341e534cd9e",
+
+		"000000000000000000000000000000000000000000000000",
+		"fe000000000000000000000000000000",
+		"8c7c27ff32bcf8dc2dc57c90c2903961",
+
+		"000000000000000000000000000000000000000000000000",
+		"ff000000000000000000000000000000",
+		"32bb6a7ec84499e166f936003d55a5bb",
+
+		"000000000000000000000000000000000000000000000000",
+		"ff800000000000000000000000000000",
+		"a5c772e5c62631ef660ee1d5877f6d1b",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffc00000000000000000000000000000",
+		"030d7e5b64f380a7e4ea5387b5cd7f49",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffe00000000000000000000000000000",
+		"0dc9a2610037009b698f11bb7e86c83e",
+
+		"000000000000000000000000000000000000000000000000",
+		"fff00000000000000000000000000000",
+		"0046612c766d1840c226364f1fa7ed72",
+
+		"000000000000000000000000000000000000000000000000",
+		"fff80000000000000000000000000000",
+		"4880c7e08f27befe78590743c05e698b",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffc0000000000000000000000000000",
+		"2520ce829a26577f0f4822c4ecc87401",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffe0000000000000000000000000000",
+		"8765e8acc169758319cb46dc7bcf3dca",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffff0000000000000000000000000000",
+		"e98f4ba4f073df4baa116d011dc24a28",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffff8000000000000000000000000000",
+		"f378f68c5dbf59e211b3a659a7317d94",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffc000000000000000000000000000",
+		"283d3b069d8eb9fb432d74b96ca762b4",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffe000000000000000000000000000",
+		"a7e1842e8a87861c221a500883245c51",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffff000000000000000000000000000",
+		"77aa270471881be070fb52c7067ce732",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffff800000000000000000000000000",
+		"01b0f476d484f43f1aeb6efa9361a8ac",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffc00000000000000000000000000",
+		"1c3a94f1c052c55c2d8359aff2163b4f",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffe00000000000000000000000000",
+		"e8a067b604d5373d8b0f2e05a03b341b",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffff00000000000000000000000000",
+		"a7876ec87f5a09bfea42c77da30fd50e",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffff80000000000000000000000000",
+		"0cf3e9d3a42be5b854ca65b13f35f48d",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffc0000000000000000000000000",
+		"6c62f6bbcab7c3e821c9290f08892dda",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffe0000000000000000000000000",
+		"7f5e05bd2068738196fee79ace7e3aec",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffff0000000000000000000000000",
+		"440e0d733255cda92fb46e842fe58054",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffff8000000000000000000000000",
+		"aa5d5b1c4ea1b7a22e5583ac2e9ed8a7",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffc000000000000000000000000",
+		"77e537e89e8491e8662aae3bc809421d",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffe000000000000000000000000",
+		"997dd3e9f1598bfa73f75973f7e93b76",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffff000000000000000000000000",
+		"1b38d4f7452afefcb7fc721244e4b72e",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffff800000000000000000000000",
+		"0be2b18252e774dda30cdda02c6906e3",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffc00000000000000000000000",
+		"d2695e59c20361d82652d7d58b6f11b2",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffe00000000000000000000000",
+		"902d88d13eae52089abd6143cfe394e9",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffff00000000000000000000000",
+		"d49bceb3b823fedd602c305345734bd2",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffff80000000000000000000000",
+		"707b1dbb0ffa40ef7d95def421233fae",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffc0000000000000000000000",
+		"7ca0c1d93356d9eb8aa952084d75f913",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffe0000000000000000000000",
+		"f2cbf9cb186e270dd7bdb0c28febc57d",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffff0000000000000000000000",
+		"c94337c37c4e790ab45780bd9c3674a0",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffff8000000000000000000000",
+		"8e3558c135252fb9c9f367ed609467a1",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffc000000000000000000000",
+		"1b72eeaee4899b443914e5b3a57fba92",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffe000000000000000000000",
+		"011865f91bc56868d051e52c9efd59b7",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffff000000000000000000000",
+		"e4771318ad7a63dd680f6e583b7747ea",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffff800000000000000000000",
+		"61e3d194088dc8d97e9e6db37457eac5",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffc00000000000000000000",
+		"36ff1ec9ccfbc349e5d356d063693ad6",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffe00000000000000000000",
+		"3cc9e9a9be8cc3f6fb2ea24088e9bb19",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffff00000000000000000000",
+		"1ee5ab003dc8722e74905d9a8fe3d350",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffff80000000000000000000",
+		"245339319584b0a412412869d6c2eada",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffc0000000000000000000",
+		"7bd496918115d14ed5380852716c8814",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffe0000000000000000000",
+		"273ab2f2b4a366a57d582a339313c8b1",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffff0000000000000000000",
+		"113365a9ffbe3b0ca61e98507554168b",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffff8000000000000000000",
+		"afa99c997ac478a0dea4119c9e45f8b1",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffc000000000000000000",
+		"9216309a7842430b83ffb98638011512",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffe000000000000000000",
+		"62abc792288258492a7cb45145f4b759",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffff000000000000000000",
+		"534923c169d504d7519c15d30e756c50",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffff800000000000000000",
+		"fa75e05bcdc7e00c273fa33f6ee441d2",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffc00000000000000000",
+		"7d350fa6057080f1086a56b17ec240db",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffe00000000000000000",
+		"f34e4a6324ea4a5c39a661c8fe5ada8f",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffff00000000000000000",
+		"0882a16f44088d42447a29ac090ec17e",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffff80000000000000000",
+		"3a3c15bfc11a9537c130687004e136ee",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffc0000000000000000",
+		"22c0a7678dc6d8cf5c8a6d5a9960767c",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffe0000000000000000",
+		"b46b09809d68b9a456432a79bdc2e38c",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffff0000000000000000",
+		"93baaffb35fbe739c17c6ac22eecf18f",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffff8000000000000000",
+		"c8aa80a7850675bc007c46df06b49868",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffc000000000000000",
+		"12c6f3877af421a918a84b775858021d",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffe000000000000000",
+		"33f123282c5d633924f7d5ba3f3cab11",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffff000000000000000",
+		"a8f161002733e93ca4527d22c1a0c5bb",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffff800000000000000",
+		"b72f70ebf3e3fda23f508eec76b42c02",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffc00000000000000",
+		"6a9d965e6274143f25afdcfc88ffd77c",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffe00000000000000",
+		"a0c74fd0b9361764ce91c5200b095357",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffff00000000000000",
+		"091d1fdc2bd2c346cd5046a8c6209146",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffff80000000000000",
+		"e2a37580116cfb71856254496ab0aca8",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffc0000000000000",
+		"e0b3a00785917c7efc9adba322813571",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffe0000000000000",
+		"733d41f4727b5ef0df4af4cf3cffa0cb",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffff0000000000000",
+		"a99ebb030260826f981ad3e64490aa4f",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffff8000000000000",
+		"73f34c7d3eae5e80082c1647524308ee",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffc000000000000",
+		"40ebd5ad082345b7a2097ccd3464da02",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffe000000000000",
+		"7cc4ae9a424b2cec90c97153c2457ec5",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffff000000000000",
+		"54d632d03aba0bd0f91877ebdd4d09cb",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffff800000000000",
+		"d3427be7e4d27cd54f5fe37b03cf0897",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffc00000000000",
+		"b2099795e88cc158fd75ea133d7e7fbe",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffe00000000000",
+		"a6cae46fb6fadfe7a2c302a34242817b",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffff00000000000",
+		"026a7024d6a902e0b3ffccbaa910cc3f",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffff80000000000",
+		"156f07767a85a4312321f63968338a01",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffc0000000000",
+		"15eec9ebf42b9ca76897d2cd6c5a12e2",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffe0000000000",
+		"db0d3a6fdcc13f915e2b302ceeb70fd8",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffff0000000000",
+		"71dbf37e87a2e34d15b20e8f10e48924",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffff8000000000",
+		"c745c451e96ff3c045e4367c833e3b54",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffc000000000",
+		"340da09c2dd11c3b679d08ccd27dd595",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffe000000000",
+		"8279f7c0c2a03ee660c6d392db025d18",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffff000000000",
+		"a4b2c7d8eba531ff47c5041a55fbd1ec",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffff800000000",
+		"74569a2ca5a7bd5131ce8dc7cbfbf72f",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffc00000000",
+		"3713da0c0219b63454035613b5a403dd",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffe00000000",
+		"8827551ddcc9df23fa72a3de4e9f0b07",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffff00000000",
+		"2e3febfd625bfcd0a2c06eb460da1732",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffff80000000",
+		"ee82e6ba488156f76496311da6941deb",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffc0000000",
+		"4770446f01d1f391256e85a1b30d89d3",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffe0000000",
+		"af04b68f104f21ef2afb4767cf74143c",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffff0000000",
+		"cf3579a9ba38c8e43653173e14f3a4c6",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffff8000000",
+		"b3bba904f4953e09b54800af2f62e7d4",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffc000000",
+		"fc4249656e14b29eb9c44829b4c59a46",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffe000000",
+		"9b31568febe81cfc2e65af1c86d1a308",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffff000000",
+		"9ca09c25f273a766db98a480ce8dfedc",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffff800000",
+		"b909925786f34c3c92d971883c9fbedf",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffc00000",
+		"82647f1332fe570a9d4d92b2ee771d3b",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffe00000",
+		"3604a7e80832b3a99954bca6f5b9f501",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffff00000",
+		"884607b128c5de3ab39a529a1ef51bef",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffff80000",
+		"670cfa093d1dbdb2317041404102435e",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffc0000",
+		"7a867195f3ce8769cbd336502fbb5130",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffe0000",
+		"52efcf64c72b2f7ca5b3c836b1078c15",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffff0000",
+		"4019250f6eefb2ac5ccbcae044e75c7e",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffff8000",
+		"022c4f6f5a017d292785627667ddef24",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffc000",
+		"e9c21078a2eb7e03250f71000fa9e3ed",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffe000",
+		"a13eaeeb9cd391da4e2b09490b3e7fad",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffff000",
+		"c958a171dca1d4ed53e1af1d380803a9",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffff800",
+		"21442e07a110667f2583eaeeee44dc8c",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffc00",
+		"59bbb353cf1dd867a6e33737af655e99",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffe00",
+		"43cd3b25375d0ce41087ff9fe2829639",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffff00",
+		"6b98b17e80d1118e3516bd768b285a84",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffff80",
+		"ae47ed3676ca0c08deea02d95b81db58",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffffc0",
+		"34ec40dc20413795ed53628ea748720b",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffffe0",
+		"4dc68163f8e9835473253542c8a65d46",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffff0",
+		"2aabb999f43693175af65c6c612c46fb",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffff8",
+		"e01f94499dac3547515c5b1d756f0f58",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffffc",
+		"9d12435a46480ce00ea349f71799df9a",
+
+		"000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffffe",
+		"cef41d16d266bdfe46938ad7884cc0cf",
+
+		"000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffffff",
+		"b13db4da1f718bc6904797c82bcf2d32",
+
+		/*
+		 * From NIST validation suite (ECBVarTxt256.rsp).
+		 */
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"80000000000000000000000000000000",
+		"ddc6bf790c15760d8d9aeb6f9a75fd4e",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"c0000000000000000000000000000000",
+		"0a6bdc6d4c1e6280301fd8e97ddbe601",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"e0000000000000000000000000000000",
+		"9b80eefb7ebe2d2b16247aa0efc72f5d",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"f0000000000000000000000000000000",
+		"7f2c5ece07a98d8bee13c51177395ff7",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"f8000000000000000000000000000000",
+		"7818d800dcf6f4be1e0e94f403d1e4c2",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fc000000000000000000000000000000",
+		"e74cd1c92f0919c35a0324123d6177d3",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fe000000000000000000000000000000",
+		"8092a4dcf2da7e77e93bdd371dfed82e",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ff000000000000000000000000000000",
+		"49af6b372135acef10132e548f217b17",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ff800000000000000000000000000000",
+		"8bcd40f94ebb63b9f7909676e667f1e7",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffc00000000000000000000000000000",
+		"fe1cffb83f45dcfb38b29be438dbd3ab",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffe00000000000000000000000000000",
+		"0dc58a8d886623705aec15cb1e70dc0e",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fff00000000000000000000000000000",
+		"c218faa16056bd0774c3e8d79c35a5e4",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fff80000000000000000000000000000",
+		"047bba83f7aa841731504e012208fc9e",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffc0000000000000000000000000000",
+		"dc8f0e4915fd81ba70a331310882f6da",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffe0000000000000000000000000000",
+		"1569859ea6b7206c30bf4fd0cbfac33c",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffff0000000000000000000000000000",
+		"300ade92f88f48fa2df730ec16ef44cd",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffff8000000000000000000000000000",
+		"1fe6cc3c05965dc08eb0590c95ac71d0",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffc000000000000000000000000000",
+		"59e858eaaa97fec38111275b6cf5abc0",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffe000000000000000000000000000",
+		"2239455e7afe3b0616100288cc5a723b",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffff000000000000000000000000000",
+		"3ee500c5c8d63479717163e55c5c4522",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffff800000000000000000000000000",
+		"d5e38bf15f16d90e3e214041d774daa8",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffc00000000000000000000000000",
+		"b1f4066e6f4f187dfe5f2ad1b17819d0",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffe00000000000000000000000000",
+		"6ef4cc4de49b11065d7af2909854794a",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffff00000000000000000000000000",
+		"ac86bc606b6640c309e782f232bf367f",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffff80000000000000000000000000",
+		"36aff0ef7bf3280772cf4cac80a0d2b2",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffc0000000000000000000000000",
+		"1f8eedea0f62a1406d58cfc3ecea72cf",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffe0000000000000000000000000",
+		"abf4154a3375a1d3e6b1d454438f95a6",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffff0000000000000000000000000",
+		"96f96e9d607f6615fc192061ee648b07",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffff8000000000000000000000000",
+		"cf37cdaaa0d2d536c71857634c792064",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffc000000000000000000000000",
+		"fbd6640c80245c2b805373f130703127",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffe000000000000000000000000",
+		"8d6a8afe55a6e481badae0d146f436db",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffff000000000000000000000000",
+		"6a4981f2915e3e68af6c22385dd06756",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffff800000000000000000000000",
+		"42a1136e5f8d8d21d3101998642d573b",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffc00000000000000000000000",
+		"9b471596dc69ae1586cee6158b0b0181",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffe00000000000000000000000",
+		"753665c4af1eff33aa8b628bf8741cfd",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffff00000000000000000000000",
+		"9a682acf40be01f5b2a4193c9a82404d",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffff80000000000000000000000",
+		"54fafe26e4287f17d1935f87eb9ade01",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffc0000000000000000000000",
+		"49d541b2e74cfe73e6a8e8225f7bd449",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffe0000000000000000000000",
+		"11a45530f624ff6f76a1b3826626ff7b",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffff0000000000000000000000",
+		"f96b0c4a8bc6c86130289f60b43b8fba",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffff8000000000000000000000",
+		"48c7d0e80834ebdc35b6735f76b46c8b",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffc000000000000000000000",
+		"2463531ab54d66955e73edc4cb8eaa45",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffe000000000000000000000",
+		"ac9bd8e2530469134b9d5b065d4f565b",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffff000000000000000000000",
+		"3f5f9106d0e52f973d4890e6f37e8a00",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffff800000000000000000000",
+		"20ebc86f1304d272e2e207e59db639f0",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffc00000000000000000000",
+		"e67ae6426bf9526c972cff072b52252c",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffe00000000000000000000",
+		"1a518dddaf9efa0d002cc58d107edfc8",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffff00000000000000000000",
+		"ead731af4d3a2fe3b34bed047942a49f",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffff80000000000000000000",
+		"b1d4efe40242f83e93b6c8d7efb5eae9",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffc0000000000000000000",
+		"cd2b1fec11fd906c5c7630099443610a",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffe0000000000000000000",
+		"a1853fe47fe29289d153161d06387d21",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffff0000000000000000000",
+		"4632154179a555c17ea604d0889fab14",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffff8000000000000000000",
+		"dd27cac6401a022e8f38f9f93e774417",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffc000000000000000000",
+		"c090313eb98674f35f3123385fb95d4d",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffe000000000000000000",
+		"cc3526262b92f02edce548f716b9f45c",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffff000000000000000000",
+		"c0838d1a2b16a7c7f0dfcc433c399c33",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffff800000000000000000",
+		"0d9ac756eb297695eed4d382eb126d26",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffc00000000000000000",
+		"56ede9dda3f6f141bff1757fa689c3e1",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffe00000000000000000",
+		"768f520efe0f23e61d3ec8ad9ce91774",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffff00000000000000000",
+		"b1144ddfa75755213390e7c596660490",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffff80000000000000000",
+		"1d7c0c4040b355b9d107a99325e3b050",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffc0000000000000000",
+		"d8e2bb1ae8ee3dcf5bf7d6c38da82a1a",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffe0000000000000000",
+		"faf82d178af25a9886a47e7f789b98d7",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffff0000000000000000",
+		"9b58dbfd77fe5aca9cfc190cd1b82d19",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffff8000000000000000",
+		"77f392089042e478ac16c0c86a0b5db5",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffc000000000000000",
+		"19f08e3420ee69b477ca1420281c4782",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffe000000000000000",
+		"a1b19beee4e117139f74b3c53fdcb875",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffff000000000000000",
+		"a37a5869b218a9f3a0868d19aea0ad6a",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffff800000000000000",
+		"bc3594e865bcd0261b13202731f33580",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffc00000000000000",
+		"811441ce1d309eee7185e8c752c07557",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffe00000000000000",
+		"959971ce4134190563518e700b9874d1",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffff00000000000000",
+		"76b5614a042707c98e2132e2e805fe63",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffff80000000000000",
+		"7d9fa6a57530d0f036fec31c230b0cc6",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffc0000000000000",
+		"964153a83bf6989a4ba80daa91c3e081",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffe0000000000000",
+		"a013014d4ce8054cf2591d06f6f2f176",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffff0000000000000",
+		"d1c5f6399bf382502e385eee1474a869",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffff8000000000000",
+		"0007e20b8298ec354f0f5fe7470f36bd",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffc000000000000",
+		"b95ba05b332da61ef63a2b31fcad9879",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffe000000000000",
+		"4620a49bd967491561669ab25dce45f4",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffff000000000000",
+		"12e71214ae8e04f0bb63d7425c6f14d5",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffff800000000000",
+		"4cc42fc1407b008fe350907c092e80ac",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffc00000000000",
+		"08b244ce7cbc8ee97fbba808cb146fda",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffe00000000000",
+		"39b333e8694f21546ad1edd9d87ed95b",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffff00000000000",
+		"3b271f8ab2e6e4a20ba8090f43ba78f3",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffff80000000000",
+		"9ad983f3bf651cd0393f0a73cccdea50",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffc0000000000",
+		"8f476cbff75c1f725ce18e4bbcd19b32",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffe0000000000",
+		"905b6267f1d6ab5320835a133f096f2a",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffff0000000000",
+		"145b60d6d0193c23f4221848a892d61a",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffff8000000000",
+		"55cfb3fb6d75cad0445bbc8dafa25b0f",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffc000000000",
+		"7b8e7098e357ef71237d46d8b075b0f5",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffe000000000",
+		"2bf27229901eb40f2df9d8398d1505ae",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffff000000000",
+		"83a63402a77f9ad5c1e931a931ecd706",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffff800000000",
+		"6f8ba6521152d31f2bada1843e26b973",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffc00000000",
+		"e5c3b8e30fd2d8e6239b17b44bd23bbd",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffe00000000",
+		"1ac1f7102c59933e8b2ddc3f14e94baa",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffff00000000",
+		"21d9ba49f276b45f11af8fc71a088e3d",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffff80000000",
+		"649f1cddc3792b4638635a392bc9bade",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffc0000000",
+		"e2775e4b59c1bc2e31a2078c11b5a08c",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffe0000000",
+		"2be1fae5048a25582a679ca10905eb80",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffff0000000",
+		"da86f292c6f41ea34fb2068df75ecc29",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffff8000000",
+		"220df19f85d69b1b562fa69a3c5beca5",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffc000000",
+		"1f11d5d0355e0b556ccdb6c7f5083b4d",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffe000000",
+		"62526b78be79cb384633c91f83b4151b",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffff000000",
+		"90ddbcb950843592dd47bbef00fdc876",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffff800000",
+		"2fd0e41c5b8402277354a7391d2618e2",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffc00000",
+		"3cdf13e72dee4c581bafec70b85f9660",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffe00000",
+		"afa2ffc137577092e2b654fa199d2c43",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffff00000",
+		"8d683ee63e60d208e343ce48dbc44cac",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffff80000",
+		"705a4ef8ba2133729c20185c3d3a4763",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffc0000",
+		"0861a861c3db4e94194211b77ed761b9",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffe0000",
+		"4b00c27e8b26da7eab9d3a88dec8b031",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffff0000",
+		"5f397bf03084820cc8810d52e5b666e9",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffff8000",
+		"63fafabb72c07bfbd3ddc9b1203104b8",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffc000",
+		"683e2140585b18452dd4ffbb93c95df9",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffe000",
+		"286894e48e537f8763b56707d7d155c8",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffff000",
+		"a423deabc173dcf7e2c4c53e77d37cd1",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffff800",
+		"eb8168313e1cfdfdb5e986d5429cf172",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffc00",
+		"27127daafc9accd2fb334ec3eba52323",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffe00",
+		"ee0715b96f72e3f7a22a5064fc592f4c",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffff00",
+		"29ee526770f2a11dcfa989d1ce88830f",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffff80",
+		"0493370e054b09871130fe49af730a5a",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffffc0",
+		"9b7b940f6c509f9e44a4ee140448ee46",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffffe0",
+		"2915be4a1ecfdcbe3e023811a12bb6c7",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffff0",
+		"7240e524bc51d8c4d440b1be55d1062c",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffff8",
+		"da63039d38cb4612b2dc36ba26684b93",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffffc",
+		"0f59cb5a4b522e2ac56c1a64f558ad9a",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"fffffffffffffffffffffffffffffffe",
+		"7bfe9d876c6d63c1d035da8fe21c409d",
+
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"ffffffffffffffffffffffffffffffff",
+		"acdace8078a32b1a182bfa4987ca1347"
+	};
+
+	/*
+	 * AES known-answer tests for CBC.
+	 * Order: key, IV, plaintext, ciphertext.
+	 */
+	static string[] KAT_AES_CBC = {
+		/*
+		 * From NIST validation suite "Multiblock Message Test"
+		 * (cbcmmt128.rsp).
+		 */
+		"1f8e4973953f3fb0bd6b16662e9a3c17",
+		"2fe2b333ceda8f98f4a99b40d2cd34a8",
+		"45cf12964fc824ab76616ae2f4bf0822",
+		"0f61c4d44c5147c03c195ad7e2cc12b2",
+
+		"0700d603a1c514e46b6191ba430a3a0c",
+		"aad1583cd91365e3bb2f0c3430d065bb",
+		"068b25c7bfb1f8bdd4cfc908f69dffc5ddc726a197f0e5f720f730393279be91",
+		"c4dc61d9725967a3020104a9738f23868527ce839aab1752fd8bdb95a82c4d00",
+
+		"3348aa51e9a45c2dbe33ccc47f96e8de",
+		"19153c673160df2b1d38c28060e59b96",
+		"9b7cee827a26575afdbb7c7a329f887238052e3601a7917456ba61251c214763d5e1847a6ad5d54127a399ab07ee3599",
+		"d5aed6c9622ec451a15db12819952b6752501cf05cdbf8cda34a457726ded97818e1f127a28d72db5652749f0c6afee5",
+
+		"b7f3c9576e12dd0db63e8f8fac2b9a39",
+		"c80f095d8bb1a060699f7c19974a1aa0",
+		"9ac19954ce1319b354d3220460f71c1e373f1cd336240881160cfde46ebfed2e791e8d5a1a136ebd1dc469dec00c4187722b841cdabcb22c1be8a14657da200e",
+		"19b9609772c63f338608bf6eb52ca10be65097f89c1e0905c42401fd47791ae2c5440b2d473116ca78bd9ff2fb6015cfd316524eae7dcb95ae738ebeae84a467",
+
+		"b6f9afbfe5a1562bba1368fc72ac9d9c",
+		"3f9d5ebe250ee7ce384b0d00ee849322",
+		"db397ec22718dbffb9c9d13de0efcd4611bf792be4fce0dc5f25d4f577ed8cdbd4eb9208d593dda3d4653954ab64f05676caa3ce9bfa795b08b67ceebc923fdc89a8c431188e9e482d8553982cf304d1",
+		"10ea27b19e16b93af169c4a88e06e35c99d8b420980b058e34b4b8f132b13766f72728202b089f428fecdb41c79f8aa0d0ef68f5786481cca29e2126f69bc14160f1ae2187878ba5c49cf3961e1b7ee9",
+
+		"bbe7b7ba07124ff1ae7c3416fe8b465e",
+		"7f65b5ee3630bed6b84202d97fb97a1e",
+		"2aad0c2c4306568bad7447460fd3dac054346d26feddbc9abd9110914011b4794be2a9a00a519a51a5b5124014f4ed2735480db21b434e99a911bb0b60fe0253763725b628d5739a5117b7ee3aefafc5b4c1bf446467e7bf5f78f31ff7caf187",
+		"3b8611bfc4973c5cd8e982b073b33184cd26110159172e44988eb5ff5661a1e16fad67258fcbfee55469267a12dc374893b4e3533d36f5634c3095583596f135aa8cd1138dc898bc5651ee35a92ebf89ab6aeb5366653bc60a70e0074fc11efe",
+
+		"89a553730433f7e6d67d16d373bd5360",
+		"f724558db3433a523f4e51a5bea70497",
+		"807bc4ea684eedcfdcca30180680b0f1ae2814f35f36d053c5aea6595a386c1442770f4d7297d8b91825ee7237241da8925dd594ccf676aecd46ca2068e8d37a3a0ec8a7d5185a201e663b5ff36ae197110188a23503763b8218826d23ced74b31e9f6e2d7fbfa6cb43420c7807a8625",
+		"406af1429a478c3d07e555c5287a60500d37fc39b68e5bbb9bafd6ddb223828561d6171a308d5b1a4551e8a5e7d572918d25c968d3871848d2f16635caa9847f38590b1df58ab5efb985f2c66cfaf86f61b3f9c0afad6c963c49cee9b8bc81a2ddb06c967f325515a4849eec37ce721a",
+
+		"c491ca31f91708458e29a925ec558d78",
+		"9ef934946e5cd0ae97bd58532cb49381",
+		"cb6a787e0dec56f9a165957f81af336ca6b40785d9e94093c6190e5152649f882e874d79ac5e167bd2a74ce5ae088d2ee854f6539e0a94796b1e1bd4c9fcdbc79acbef4d01eeb89776d18af71ae2a4fc47dd66df6c4dbe1d1850e466549a47b636bcc7c2b3a62495b56bb67b6d455f1eebd9bfefecbca6c7f335cfce9b45cb9d",
+		"7b2931f5855f717145e00f152a9f4794359b1ffcb3e55f594e33098b51c23a6c74a06c1d94fded7fd2ae42c7db7acaef5844cb33aeddc6852585ed0020a6699d2cb53809cefd169148ce42292afab063443978306c582c18b9ce0da3d084ce4d3c482cfd8fcf1a85084e89fb88b40a084d5e972466d07666126fb761f84078f2",
+
+		"f6e87d71b0104d6eb06a68dc6a71f498",
+		"1c245f26195b76ebebc2edcac412a2f8",
+		"f82bef3c73a6f7f80db285726d691db6bf55eec25a859d3ba0e0445f26b9bb3b16a3161ed1866e4dd8f2e5f8ecb4e46d74a7a78c20cdfc7bcc9e479ba7a0caba9438238ad0c01651d5d98de37f03ddce6e6b4bd4ab03cf9e8ed818aedfa1cf963b932067b97d776dce1087196e7e913f7448e38244509f0caf36bd8217e15336d35c149fd4e41707893fdb84014f8729",
+		"b09512f3eff9ed0d85890983a73dadbb7c3678d52581be64a8a8fc586f490f2521297a478a0598040ebd0f5509fafb0969f9d9e600eaef33b1b93eed99687b167f89a5065aac439ce46f3b8d22d30865e64e45ef8cd30b6984353a844a11c8cd60dba0e8866b3ee30d24b3fa8a643b328353e06010fa8273c8fd54ef0a2b6930e5520aae5cd5902f9b86a33592ca4365",
+
+		"2c14413751c31e2730570ba3361c786b",
+		"1dbbeb2f19abb448af849796244a19d7",
+		"40d930f9a05334d9816fe204999c3f82a03f6a0457a8c475c94553d1d116693adc618049f0a769a2eed6a6cb14c0143ec5cccdbc8dec4ce560cfd206225709326d4de7948e54d603d01b12d7fed752fb23f1aa4494fbb00130e9ded4e77e37c079042d828040c325b1a5efd15fc842e44014ca4374bf38f3c3fc3ee327733b0c8aee1abcd055772f18dc04603f7b2c1ea69ff662361f2be0a171bbdcea1e5d3f",
+		"6be8a12800455a320538853e0cba31bd2d80ea0c85164a4c5c261ae485417d93effe2ebc0d0a0b51d6ea18633d210cf63c0c4ddbc27607f2e81ed9113191ef86d56f3b99be6c415a4150299fb846ce7160b40b63baf1179d19275a2e83698376d28b92548c68e06e6d994e2c1501ed297014e702cdefee2f656447706009614d801de1caaf73f8b7fa56cf1ba94b631933bbe577624380850f117435a0355b2b",
+
+		/*
+		 * From NIST validation suite "Multiblock Message Test"
+		 * (cbcmmt192.rsp).
+		 */
+		"ba75f4d1d9d7cf7f551445d56cc1a8ab2a078e15e049dc2c",
+		"531ce78176401666aa30db94ec4a30eb",
+		"c51fc276774dad94bcdc1d2891ec8668",
+		"70dd95a14ee975e239df36ff4aee1d5d",
+
+		"eab3b19c581aa873e1981c83ab8d83bbf8025111fb2e6b21",
+		"f3d6667e8d4d791e60f7505ba383eb05",
+		"9d4e4cccd1682321856df069e3f1c6fa391a083a9fb02d59db74c14081b3acc4",
+		"51d44779f90d40a80048276c035cb49ca2a47bcb9b9cf7270b9144793787d53f",
+
+		"16c93bb398f1fc0cf6d68fc7a5673cdf431fa147852b4a2d",
+		"eaaeca2e07ddedf562f94df63f0a650f",
+		"c5ce958613bf741718c17444484ebaf1050ddcacb59b9590178cbe69d7ad7919608cb03af13bbe04f3506b718a301ea0",
+		"ed6a50e0c6921d52d6647f75d67b4fd56ace1fedb8b5a6a997b4d131640547d22c5d884a75e6752b5846b5b33a5181f4",
+
+		"067bb17b4df785697eaccf961f98e212cb75e6797ce935cb",
+		"8b59c9209c529ca8391c9fc0ce033c38",
+		"db3785a889b4bd387754da222f0e4c2d2bfe0d79e05bc910fba941beea30f1239eacf0068f4619ec01c368e986fca6b7c58e490579d29611bd10087986eff54f",
+		"d5f5589760bf9c762228fde236de1fa2dd2dad448db3fa9be0c4196efd46a35c84dd1ac77d9db58c95918cb317a6430a08d2fb6a8e8b0f1c9b72c7a344dc349f",
+
+		"0fd39de83e0be77a79c8a4a612e3dd9c8aae2ce35e7a2bf8",
+		"7e1d629b84f93b079be51f9a5f5cb23c",
+		"38fbda37e28fa86d9d83a4345e419dea95d28c7818ff25925db6ac3aedaf0a86154e20a4dfcc5b1b4192895393e5eb5846c88bdbd41ecf7af3104f410eaee470f5d9017ed460475f626953035a13db1f",
+		"edadae2f9a45ff3473e02d904c94d94a30a4d92da4deb6bcb4b0774472694571842039f21c496ef93fd658842c735f8a81fcd0aa578442ab893b18f606aed1bab11f81452dd45e9b56adf2eccf4ea095",
+
+		"e3fecc75f0075a09b383dfd389a3d33cc9b854b3b254c0f4",
+		"36eab883afef936cc38f63284619cd19",
+		"931b2f5f3a5820d53a6beaaa6431083a3488f4eb03b0f5b57ef838e1579623103bd6e6800377538b2e51ef708f3c4956432e8a8ee6a34e190642b26ad8bdae6c2af9a6c7996f3b6004d2671e41f1c9f40ee03d1c4a52b0a0654a331f15f34dce",
+		"75395974bd32b3665654a6c8e396b88ae34b123575872a7ab687d8e76b46df911a8a590cd01d2f5c330be3a6626e9dd3aa5e10ed14e8ff829811b6fed50f3f533ca4385a1cbca78f5c4744e50f2f8359165c2485d1324e76c3eae76a0ccac629",
+
+		"f9c27565eb07947c8cb51b79248430f7b1066c3d2fdc3d13",
+		"2bd67cc89ab7948d644a49672843cbd9",
+		"6abcc270173cf114d44847e911a050db57ba7a2e2c161c6f37ccb6aaa4677bddcaf50cad0b5f8758fcf7c0ebc650ceb5cd52cafb8f8dd3edcece55d9f1f08b9fa8f54365cf56e28b9596a7e1dd1d3418e4444a7724add4cf79d527b183ec88de4be4eeff29c80a97e54f85351cb189ee",
+		"ca282924a61187feb40520979106e5cc861957f23828dcb7285e0eaac8a0ca2a6b60503d63d6039f4693dba32fa1f73ae2e709ca94911f28a5edd1f30eaddd54680c43acc9c74cd90d8bb648b4e544275f47e514daa20697f66c738eb30337f017fca1a26da4d1a0cc0a0e98e2463070",
+
+		"fb09cf9e00dbf883689d079c920077c0073c31890b55bab5",
+		"e3c89bd097c3abddf64f4881db6dbfe2",
+		"c1a37683fb289467dd1b2c89efba16bbd2ee24cf18d19d44596ded2682c79a2f711c7a32bf6a24badd32a4ee637c73b7a41da6258635650f91fb9ffa45bdfc3cb122136241b3deced8996aa51ea8d3e81c9d70e006a44bc0571ed48623a0d622a93fa9da290baaedf5d9e876c94620945ff8ecc83f27379ed55cf490c5790f27",
+		"8158e21420f25b59d6ae943fa1cbf21f02e979f419dab0126a721b7eef55bee9ad97f5ccff7d239057bbc19a8c378142f7672f1d5e7e17d7bebcb0070e8355cace6660171a53b61816ae824a6ef69ce470b6ffd3b5bb4b438874d91d27854d3b6f25860d3868958de3307d62b1339bdddb8a318c0ce0f33c17caf0e9f6040820",
+
+		"bca6fa3c67fd294e958f66fe8bd64f45f428f5bc8e9733a7",
+		"92a47f2833f1450d1da41717bdc6e83c",
+		"5becbc31d8bead6d36ae014a5863d14a431e6b55d29ea6baaa417271716db3a33b2e506b452086dfe690834ac2de30bc41254ec5401ec47d064237c7792fdcd7914d8af20eb114756642d519021a8c75a92f6bc53d326ae9a5b7e1b10a9756574692934d9939fc399e0c203f7edf8e7e6482eadd31a0400770e897b48c6bca2b404593045080e93377358c42a0f4dede",
+		"926db248cc1ba20f0c57631a7c8aef094f791937b905949e3460240e8bfa6fa483115a1b310b6e4369caebc5262888377b1ddaa5800ea496a2bdff0f9a1031e7129c9a20e35621e7f0b8baca0d87030f2ae7ca8593c8599677a06fd4b26009ead08fecac24caa9cf2cad3b470c8227415a7b1e0f2eab3fad96d70a209c8bb26c627677e2531b9435ca6e3c444d195b5f",
+
+		"162ad50ee64a0702aa551f571dedc16b2c1b6a1e4d4b5eee",
+		"24408038161a2ccae07b029bb66355c1",
+		"be8abf00901363987a82cc77d0ec91697ba3857f9e4f84bd79406c138d02698f003276d0449120bef4578d78fecabe8e070e11710b3f0a2744bd52434ec70015884c181ebdfd51c604a71c52e4c0e110bc408cd462b248a80b8a8ac06bb952ac1d7faed144807f1a731b7febcaf7835762defe92eccfc7a9944e1c702cffe6bc86733ed321423121085ac02df8962bcbc1937092eebf0e90a8b20e3dd8c244ae",
+		"c82cf2c476dea8cb6a6e607a40d2f0391be82ea9ec84a537a6820f9afb997b76397d005424faa6a74dc4e8c7aa4a8900690f894b6d1dca80675393d2243adac762f159301e357e98b724762310cd5a7bafe1c2a030dba46fd93a9fdb89cc132ca9c17dc72031ec6822ee5a9d99dbca66c784c01b0885cbb62e29d97801927ec415a5d215158d325f9ee689437ad1b7684ad33c0d92739451ac87f39ff8c31b84",
+
+		/*
+		 * From NIST validation suite "Multiblock Message Test"
+		 * (cbcmmt256.rsp).
+		 */
+		"6ed76d2d97c69fd1339589523931f2a6cff554b15f738f21ec72dd97a7330907",
+		"851e8764776e6796aab722dbb644ace8",
+		"6282b8c05c5c1530b97d4816ca434762",
+		"6acc04142e100a65f51b97adf5172c41",
+
+		"dce26c6b4cfb286510da4eecd2cffe6cdf430f33db9b5f77b460679bd49d13ae",
+		"fdeaa134c8d7379d457175fd1a57d3fc",
+		"50e9eee1ac528009e8cbcd356975881f957254b13f91d7c6662d10312052eb00",
+		"2fa0df722a9fd3b64cb18fb2b3db55ff2267422757289413f8f657507412a64c",
+
+		"fe8901fecd3ccd2ec5fdc7c7a0b50519c245b42d611a5ef9e90268d59f3edf33",
+		"bd416cb3b9892228d8f1df575692e4d0",
+		"8d3aa196ec3d7c9b5bb122e7fe77fb1295a6da75abe5d3a510194d3a8a4157d5c89d40619716619859da3ec9b247ced9",
+		"608e82c7ab04007adb22e389a44797fed7de090c8c03ca8a2c5acd9e84df37fbc58ce8edb293e98f02b640d6d1d72464",
+
+		"0493ff637108af6a5b8e90ac1fdf035a3d4bafd1afb573be7ade9e8682e663e5",
+		"c0cd2bebccbb6c49920bd5482ac756e8",
+		"8b37f9148df4bb25956be6310c73c8dc58ea9714ff49b643107b34c9bff096a94fedd6823526abc27a8e0b16616eee254ab4567dd68e8ccd4c38ac563b13639c",
+		"05d5c77729421b08b737e41119fa4438d1f570cc772a4d6c3df7ffeda0384ef84288ce37fc4c4c7d1125a499b051364c389fd639bdda647daa3bdadab2eb5594",
+
+		"9adc8fbd506e032af7fa20cf5343719de6d1288c158c63d6878aaf64ce26ca85",
+		"11958dc6ab81e1c7f01631e9944e620f",
+		"c7917f84f747cd8c4b4fedc2219bdbc5f4d07588389d8248854cf2c2f89667a2d7bcf53e73d32684535f42318e24cd45793950b3825e5d5c5c8fcd3e5dda4ce9246d18337ef3052d8b21c5561c8b660e",
+		"9c99e68236bb2e929db1089c7750f1b356d39ab9d0c40c3e2f05108ae9d0c30b04832ccdbdc08ebfa426b7f5efde986ed05784ce368193bb3699bc691065ac62e258b9aa4cc557e2b45b49ce05511e65",
+
+		"73b8faf00b3302ac99855cf6f9e9e48518690a5906a4869d4dcf48d282faae2a",
+		"b3cb97a80a539912b8c21f450d3b9395",
+		"3adea6e06e42c4f041021491f2775ef6378cb08824165edc4f6448e232175b60d0345b9f9c78df6596ec9d22b7b9e76e8f3c76b32d5d67273f1d83fe7a6fc3dd3c49139170fa5701b3beac61b490f0a9e13f844640c4500f9ad3087adfb0ae10",
+		"ac3d6dbafe2e0f740632fd9e820bf6044cd5b1551cbb9cc03c0b25c39ccb7f33b83aacfca40a3265f2bbff879153448acacb88fcfb3bb7b10fe463a68c0109f028382e3e557b1adf02ed648ab6bb895df0205d26ebbfa9a5fd8cebd8e4bee3dc",
+
+		"9ddf3745896504ff360a51a3eb49c01b79fccebc71c3abcb94a949408b05b2c9",
+		"e79026639d4aa230b5ccffb0b29d79bc",
+		"cf52e5c3954c51b94c9e38acb8c9a7c76aebdaa9943eae0a1ce155a2efdb4d46985d935511471452d9ee64d2461cb2991d59fc0060697f9a671672163230f367fed1422316e52d29eceacb8768f56d9b80f6d278093c9a8acd3cfd7edd8ebd5c293859f64d2f8486ae1bd593c65bc014",
+		"34df561bd2cfebbcb7af3b4b8d21ca5258312e7e2e4e538e35ad2490b6112f0d7f148f6aa8d522a7f3c61d785bd667db0e1dc4606c318ea4f26af4fe7d11d4dcff0456511b4aed1a0d91ba4a1fd6cd9029187bc5881a5a07fe02049d39368e83139b12825bae2c7be81e6f12c61bb5c5",
+
+		"458b67bf212d20f3a57fce392065582dcefbf381aa22949f8338ab9052260e1d",
+		"4c12effc5963d40459602675153e9649",
+		"256fd73ce35ae3ea9c25dd2a9454493e96d8633fe633b56176dce8785ce5dbbb84dbf2c8a2eeb1e96b51899605e4f13bbc11b93bf6f39b3469be14858b5b720d4a522d36feed7a329c9b1e852c9280c47db8039c17c4921571a07d1864128330e09c308ddea1694e95c84500f1a61e614197e86a30ecc28df64ccb3ccf5437aa",
+		"90b7b9630a2378f53f501ab7beff039155008071bc8438e789932cfd3eb1299195465e6633849463fdb44375278e2fdb1310821e6492cf80ff15cb772509fb426f3aeee27bd4938882fd2ae6b5bd9d91fa4a43b17bb439ebbe59c042310163a82a5fe5388796eee35a181a1271f00be29b852d8fa759bad01ff4678f010594cd",
+
+		"d2412db0845d84e5732b8bbd642957473b81fb99ca8bff70e7920d16c1dbec89",
+		"51c619fcf0b23f0c7925f400a6cacb6d",
+		"026006c4a71a180c9929824d9d095b8faaa86fc4fa25ecac61d85ff6de92dfa8702688c02a282c1b8af4449707f22d75e91991015db22374c95f8f195d5bb0afeb03040ff8965e0e1339dba5653e174f8aa5a1b39fe3ac839ce307a4e44b4f8f1b0063f738ec18acdbff2ebfe07383e734558723e741f0a1836dafdf9de82210a9248bc113b3c1bc8b4e252ca01bd803",
+		"0254b23463bcabec5a395eb74c8fb0eb137a07bc6f5e9f61ec0b057de305714f8fa294221c91a159c315939b81e300ee902192ec5f15254428d8772f79324ec43298ca21c00b370273ee5e5ed90e43efa1e05a5d171209fe34f9f29237dba2a6726650fd3b1321747d1208863c6c3c6b3e2d879ab5f25782f08ba8f2abbe63e0bedb4a227e81afb36bb6645508356d34",
+
+		"48be597e632c16772324c8d3fa1d9c5a9ecd010f14ec5d110d3bfec376c5532b",
+		"d6d581b8cf04ebd3b6eaa1b53f047ee1",
+		"0c63d413d3864570e70bb6618bf8a4b9585586688c32bba0a5ecc1362fada74ada32c52acfd1aa7444ba567b4e7daaecf7cc1cb29182af164ae5232b002868695635599807a9a7f07a1f137e97b1e1c9dabc89b6a5e4afa9db5855edaa575056a8f4f8242216242bb0c256310d9d329826ac353d715fa39f80cec144d6424558f9f70b98c920096e0f2c855d594885a00625880e9dfb734163cecef72cf030b8",
+		"fc5873e50de8faf4c6b84ba707b0854e9db9ab2e9f7d707fbba338c6843a18fc6facebaf663d26296fb329b4d26f18494c79e09e779647f9bafa87489630d79f4301610c2300c19dbf3148b7cac8c4f4944102754f332e92b6f7c5e75bc6179eb877a078d4719009021744c14f13fd2a55a2b9c44d18000685a845a4f632c7c56a77306efa66a24d05d088dcd7c13fe24fc447275965db9e4d37fbc9304448cd"
+	};
+
+	/*
+	 * AES known-answer tests for CTR.
+	 * Order: key, IV, plaintext, ciphertext.
+	 */
+	static string[] KAT_AES_CTR = {
+		/*
+		 * From RFC 3686.
+		 */
+		"ae6852f8121067cc4bf7a5765577f39e",
+		"000000300000000000000000",
+		"53696e676c6520626c6f636b206d7367",
+		"e4095d4fb7a7b3792d6175a3261311b8",
+
+		"7e24067817fae0d743d6ce1f32539163",
+		"006cb6dbc0543b59da48d90b",
+		"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
+		"5104a106168a72d9790d41ee8edad388eb2e1efc46da57c8fce630df9141be28",
+
+		"7691be035e5020a8ac6e618529f9a0dc",
+		"00e0017b27777f3f4a1786f0",
+		"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223",
+		"c1cf48a89f2ffdd9cf4652e9efdb72d74540a42bde6d7836d59a5ceaaef3105325b2072f",
+
+		"16af5b145fc9f579c175f93e3bfb0eed863d06ccfdb78515",
+		"0000004836733c147d6d93cb",
+		"53696e676c6520626c6f636b206d7367",
+		"4b55384fe259c9c84e7935a003cbe928",
+
+		"7c5cb2401b3dc33c19e7340819e0f69c678c3db8e6f6a91a",
+		"0096b03b020c6eadc2cb500d",
+		"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
+		"453243fc609b23327edfaafa7131cd9f8490701c5ad4a79cfc1fe0ff42f4fb00",
+
+		"02bf391ee8ecb159b959617b0965279bf59b60a786d3e0fe",
+		"0007bdfd5cbd60278dcc0912",
+		"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223",
+		"96893fc55e5c722f540b7dd1ddf7e758d288bc95c69165884536c811662f2188abee0935",
+
+		"776beff2851db06f4c8a0542c8696f6c6a81af1eec96b4d37fc1d689e6c1c104",
+		"00000060db5672c97aa8f0b2",
+		"53696e676c6520626c6f636b206d7367",
+		"145ad01dbf824ec7560863dc71e3e0c0",
+
+		"f6d66d6bd52d59bb0796365879eff886c66dd51a5b6a99744b50590c87a23884",
+		"00faac24c1585ef15a43d875",
+		"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f",
+		"f05e231b3894612c49ee000b804eb2a9b8306b508f839d6a5530831d9344af1c",
+
+		"ff7a617ce69148e4f1726e2f43581de2aa62d9f805532edff1eed687fb54153d",
+		"001cc5b751a51d70a1c11148",
+		"000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20212223",
+		"eb6c52821d0bbbf7ce7594462aca4faab407df866569fd07f48cc0b583d6071f1ec0e6b8"
+	};
+
+	/*
+	 * DES known-answer tests.
+	 * Order: plaintext, key, ciphertext.
+	 * (mostly from NIST SP 800-20).
+	 */
+	static string[] KAT_DES_RAW = {
+		"10316E028C8F3B4A", "0000000000000000", "82DCBAFBDEAB6602",
+		"8000000000000000", "0000000000000000", "95A8D72813DAA94D",
+		"4000000000000000", "0000000000000000", "0EEC1487DD8C26D5",
+		"2000000000000000", "0000000000000000", "7AD16FFB79C45926",
+		"1000000000000000", "0000000000000000", "D3746294CA6A6CF3",
+		"0800000000000000", "0000000000000000", "809F5F873C1FD761",
+		"0400000000000000", "0000000000000000", "C02FAFFEC989D1FC",
+		"0200000000000000", "0000000000000000", "4615AA1D33E72F10",
+		"0100000000000000", "0000000000000000", "8CA64DE9C1B123A7",
+		"0080000000000000", "0000000000000000", "2055123350C00858",
+		"0040000000000000", "0000000000000000", "DF3B99D6577397C8",
+		"0020000000000000", "0000000000000000", "31FE17369B5288C9",
+		"0010000000000000", "0000000000000000", "DFDD3CC64DAE1642",
+		"0008000000000000", "0000000000000000", "178C83CE2B399D94",
+		"0004000000000000", "0000000000000000", "50F636324A9B7F80",
+		"0002000000000000", "0000000000000000", "A8468EE3BC18F06D",
+		"0001000000000000", "0000000000000000", "8CA64DE9C1B123A7",
+		"0000800000000000", "0000000000000000", "A2DC9E92FD3CDE92",
+		"0000400000000000", "0000000000000000", "CAC09F797D031287",
+		"0000200000000000", "0000000000000000", "90BA680B22AEB525",
+		"0000100000000000", "0000000000000000", "CE7A24F350E280B6",
+		"0000080000000000", "0000000000000000", "882BFF0AA01A0B87",
+		"0000040000000000", "0000000000000000", "25610288924511C2",
+		"0000020000000000", "0000000000000000", "C71516C29C75D170",
+		"0000010000000000", "0000000000000000", "8CA64DE9C1B123A7",
+		"0000008000000000", "0000000000000000", "5199C29A52C9F059",
+		"0000004000000000", "0000000000000000", "C22F0A294A71F29F",
+		"0000002000000000", "0000000000000000", "EE371483714C02EA",
+		"0000001000000000", "0000000000000000", "A81FBD448F9E522F",
+		"0000000800000000", "0000000000000000", "4F644C92E192DFED",
+		"0000000400000000", "0000000000000000", "1AFA9A66A6DF92AE",
+		"0000000200000000", "0000000000000000", "B3C1CC715CB879D8",
+		"0000000100000000", "0000000000000000", "8CA64DE9C1B123A7",
+		"0000000080000000", "0000000000000000", "19D032E64AB0BD8B",
+		"0000000040000000", "0000000000000000", "3CFAA7A7DC8720DC",
+		"0000000020000000", "0000000000000000", "B7265F7F447AC6F3",
+		"0000000010000000", "0000000000000000", "9DB73B3C0D163F54",
+		"0000000008000000", "0000000000000000", "8181B65BABF4A975",
+		"0000000004000000", "0000000000000000", "93C9B64042EAA240",
+		"0000000002000000", "0000000000000000", "5570530829705592",
+		"0000000001000000", "0000000000000000", "8CA64DE9C1B123A7",
+		"0000000000800000", "0000000000000000", "8638809E878787A0",
+		"0000000000400000", "0000000000000000", "41B9A79AF79AC208",
+		"0000000000200000", "0000000000000000", "7A9BE42F2009A892",
+		"0000000000100000", "0000000000000000", "29038D56BA6D2745",
+		"0000000000080000", "0000000000000000", "5495C6ABF1E5DF51",
+		"0000000000040000", "0000000000000000", "AE13DBD561488933",
+		"0000000000020000", "0000000000000000", "024D1FFA8904E389",
+		"0000000000010000", "0000000000000000", "8CA64DE9C1B123A7",
+		"0000000000008000", "0000000000000000", "D1399712F99BF02E",
+		"0000000000004000", "0000000000000000", "14C1D7C1CFFEC79E",
+		"0000000000002000", "0000000000000000", "1DE5279DAE3BED6F",
+		"0000000000001000", "0000000000000000", "E941A33F85501303",
+		"0000000000000800", "0000000000000000", "DA99DBBC9A03F379",
+		"0000000000000400", "0000000000000000", "B7FC92F91D8E92E9",
+		"0000000000000200", "0000000000000000", "AE8E5CAA3CA04E85",
+		"0000000000000100", "0000000000000000", "8CA64DE9C1B123A7",
+		"0000000000000080", "0000000000000000", "9CC62DF43B6EED74",
+		"0000000000000040", "0000000000000000", "D863DBB5C59A91A0",
+		"0000000000000020", "0000000000000000", "A1AB2190545B91D7",
+		"0000000000000010", "0000000000000000", "0875041E64C570F7",
+		"0000000000000008", "0000000000000000", "5A594528BEBEF1CC",
+		"0000000000000004", "0000000000000000", "FCDB3291DE21F0C0",
+		"0000000000000002", "0000000000000000", "869EFD7F9F265A09",
+		"0000000000000001", "0000000000000000", "8CA64DE9C1B123A7",
+		"0000000000000000", "8000000000000000", "95F8A5E5DD31D900",
+		"0000000000000000", "4000000000000000", "DD7F121CA5015619",
+		"0000000000000000", "2000000000000000", "2E8653104F3834EA",
+		"0000000000000000", "1000000000000000", "4BD388FF6CD81D4F",
+		"0000000000000000", "0800000000000000", "20B9E767B2FB1456",
+		"0000000000000000", "0400000000000000", "55579380D77138EF",
+		"0000000000000000", "0200000000000000", "6CC5DEFAAF04512F",
+		"0000000000000000", "0100000000000000", "0D9F279BA5D87260",
+		"0000000000000000", "0080000000000000", "D9031B0271BD5A0A",
+		"0000000000000000", "0040000000000000", "424250B37C3DD951",
+		"0000000000000000", "0020000000000000", "B8061B7ECD9A21E5",
+		"0000000000000000", "0010000000000000", "F15D0F286B65BD28",
+		"0000000000000000", "0008000000000000", "ADD0CC8D6E5DEBA1",
+		"0000000000000000", "0004000000000000", "E6D5F82752AD63D1",
+		"0000000000000000", "0002000000000000", "ECBFE3BD3F591A5E",
+		"0000000000000000", "0001000000000000", "F356834379D165CD",
+		"0000000000000000", "0000800000000000", "2B9F982F20037FA9",
+		"0000000000000000", "0000400000000000", "889DE068A16F0BE6",
+		"0000000000000000", "0000200000000000", "E19E275D846A1298",
+		"0000000000000000", "0000100000000000", "329A8ED523D71AEC",
+		"0000000000000000", "0000080000000000", "E7FCE22557D23C97",
+		"0000000000000000", "0000040000000000", "12A9F5817FF2D65D",
+		"0000000000000000", "0000020000000000", "A484C3AD38DC9C19",
+		"0000000000000000", "0000010000000000", "FBE00A8A1EF8AD72",
+		"0000000000000000", "0000008000000000", "750D079407521363",
+		"0000000000000000", "0000004000000000", "64FEED9C724C2FAF",
+		"0000000000000000", "0000002000000000", "F02B263B328E2B60",
+		"0000000000000000", "0000001000000000", "9D64555A9A10B852",
+		"0000000000000000", "0000000800000000", "D106FF0BED5255D7",
+		"0000000000000000", "0000000400000000", "E1652C6B138C64A5",
+		"0000000000000000", "0000000200000000", "E428581186EC8F46",
+		"0000000000000000", "0000000100000000", "AEB5F5EDE22D1A36",
+		"0000000000000000", "0000000080000000", "E943D7568AEC0C5C",
+		"0000000000000000", "0000000040000000", "DF98C8276F54B04B",
+		"0000000000000000", "0000000020000000", "B160E4680F6C696F",
+		"0000000000000000", "0000000010000000", "FA0752B07D9C4AB8",
+		"0000000000000000", "0000000008000000", "CA3A2B036DBC8502",
+		"0000000000000000", "0000000004000000", "5E0905517BB59BCF",
+		"0000000000000000", "0000000002000000", "814EEB3B91D90726",
+		"0000000000000000", "0000000001000000", "4D49DB1532919C9F",
+		"0000000000000000", "0000000000800000", "25EB5FC3F8CF0621",
+		"0000000000000000", "0000000000400000", "AB6A20C0620D1C6F",
+		"0000000000000000", "0000000000200000", "79E90DBC98F92CCA",
+		"0000000000000000", "0000000000100000", "866ECEDD8072BB0E",
+		"0000000000000000", "0000000000080000", "8B54536F2F3E64A8",
+		"0000000000000000", "0000000000040000", "EA51D3975595B86B",
+		"0000000000000000", "0000000000020000", "CAFFC6AC4542DE31",
+		"0000000000000000", "0000000000010000", "8DD45A2DDF90796C",
+		"0000000000000000", "0000000000008000", "1029D55E880EC2D0",
+		"0000000000000000", "0000000000004000", "5D86CB23639DBEA9",
+		"0000000000000000", "0000000000002000", "1D1CA853AE7C0C5F",
+		"0000000000000000", "0000000000001000", "CE332329248F3228",
+		"0000000000000000", "0000000000000800", "8405D1ABE24FB942",
+		"0000000000000000", "0000000000000400", "E643D78090CA4207",
+		"0000000000000000", "0000000000000200", "48221B9937748A23",
+		"0000000000000000", "0000000000000100", "DD7C0BBD61FAFD54",
+		"0000000000000000", "0000000000000080", "2FBC291A570DB5C4",
+		"0000000000000000", "0000000000000040", "E07C30D7E4E26E12",
+		"0000000000000000", "0000000000000020", "0953E2258E8E90A1",
+		"0000000000000000", "0000000000000010", "5B711BC4CEEBF2EE",
+		"0000000000000000", "0000000000000008", "CC083F1E6D9E85F6",
+		"0000000000000000", "0000000000000004", "D2FD8867D50D2DFE",
+		"0000000000000000", "0000000000000002", "06E7EA22CE92708F",
+		"0000000000000000", "0000000000000001", "166B40B44ABA4BD6",
+		"0000000000000000", "0000000000000000", "8CA64DE9C1B123A7",
+		"0101010101010101", "0101010101010101", "994D4DC157B96C52",
+		"0202020202020202", "0202020202020202", "E127C2B61D98E6E2",
+		"0303030303030303", "0303030303030303", "984C91D78A269CE3",
+		"0404040404040404", "0404040404040404", "1F4570BB77550683",
+		"0505050505050505", "0505050505050505", "3990ABF98D672B16",
+		"0606060606060606", "0606060606060606", "3F5150BBA081D585",
+		"0707070707070707", "0707070707070707", "C65242248C9CF6F2",
+		"0808080808080808", "0808080808080808", "10772D40FAD24257",
+		"0909090909090909", "0909090909090909", "F0139440647A6E7B",
+		"0A0A0A0A0A0A0A0A", "0A0A0A0A0A0A0A0A", "0A288603044D740C",
+		"0B0B0B0B0B0B0B0B", "0B0B0B0B0B0B0B0B", "6359916942F7438F",
+		"0C0C0C0C0C0C0C0C", "0C0C0C0C0C0C0C0C", "934316AE443CF08B",
+		"0D0D0D0D0D0D0D0D", "0D0D0D0D0D0D0D0D", "E3F56D7F1130A2B7",
+		"0E0E0E0E0E0E0E0E", "0E0E0E0E0E0E0E0E", "A2E4705087C6B6B4",
+		"0F0F0F0F0F0F0F0F", "0F0F0F0F0F0F0F0F", "D5D76E09A447E8C3",
+		"1010101010101010", "1010101010101010", "DD7515F2BFC17F85",
+		"1111111111111111", "1111111111111111", "F40379AB9E0EC533",
+		"1212121212121212", "1212121212121212", "96CD27784D1563E5",
+		"1313131313131313", "1313131313131313", "2911CF5E94D33FE1",
+		"1414141414141414", "1414141414141414", "377B7F7CA3E5BBB3",
+		"1515151515151515", "1515151515151515", "701AA63832905A92",
+		"1616161616161616", "1616161616161616", "2006E716C4252D6D",
+		"1717171717171717", "1717171717171717", "452C1197422469F8",
+		"1818181818181818", "1818181818181818", "C33FD1EB49CB64DA",
+		"1919191919191919", "1919191919191919", "7572278F364EB50D",
+		"1A1A1A1A1A1A1A1A", "1A1A1A1A1A1A1A1A", "69E51488403EF4C3",
+		"1B1B1B1B1B1B1B1B", "1B1B1B1B1B1B1B1B", "FF847E0ADF192825",
+		"1C1C1C1C1C1C1C1C", "1C1C1C1C1C1C1C1C", "521B7FB3B41BB791",
+		"1D1D1D1D1D1D1D1D", "1D1D1D1D1D1D1D1D", "26059A6A0F3F6B35",
+		"1E1E1E1E1E1E1E1E", "1E1E1E1E1E1E1E1E", "F24A8D2231C77538",
+		"1F1F1F1F1F1F1F1F", "1F1F1F1F1F1F1F1F", "4FD96EC0D3304EF6",
+		"2020202020202020", "2020202020202020", "18A9D580A900B699",
+		"2121212121212121", "2121212121212121", "88586E1D755B9B5A",
+		"2222222222222222", "2222222222222222", "0F8ADFFB11DC2784",
+		"2323232323232323", "2323232323232323", "2F30446C8312404A",
+		"2424242424242424", "2424242424242424", "0BA03D9E6C196511",
+		"2525252525252525", "2525252525252525", "3E55E997611E4B7D",
+		"2626262626262626", "2626262626262626", "B2522FB5F158F0DF",
+		"2727272727272727", "2727272727272727", "2109425935406AB8",
+		"2828282828282828", "2828282828282828", "11A16028F310FF16",
+		"2929292929292929", "2929292929292929", "73F0C45F379FE67F",
+		"2A2A2A2A2A2A2A2A", "2A2A2A2A2A2A2A2A", "DCAD4338F7523816",
+		"2B2B2B2B2B2B2B2B", "2B2B2B2B2B2B2B2B", "B81634C1CEAB298C",
+		"2C2C2C2C2C2C2C2C", "2C2C2C2C2C2C2C2C", "DD2CCB29B6C4C349",
+		"2D2D2D2D2D2D2D2D", "2D2D2D2D2D2D2D2D", "7D07A77A2ABD50A7",
+		"2E2E2E2E2E2E2E2E", "2E2E2E2E2E2E2E2E", "30C1B0C1FD91D371",
+		"2F2F2F2F2F2F2F2F", "2F2F2F2F2F2F2F2F", "C4427B31AC61973B",
+		"3030303030303030", "3030303030303030", "F47BB46273B15EB5",
+		"3131313131313131", "3131313131313131", "655EA628CF62585F",
+		"3232323232323232", "3232323232323232", "AC978C247863388F",
+		"3333333333333333", "3333333333333333", "0432ED386F2DE328",
+		"3434343434343434", "3434343434343434", "D254014CB986B3C2",
+		"3535353535353535", "3535353535353535", "B256E34BEDB49801",
+		"3636363636363636", "3636363636363636", "37F8759EB77E7BFC",
+		"3737373737373737", "3737373737373737", "5013CA4F62C9CEA0",
+		"3838383838383838", "3838383838383838", "8940F7B3EACA5939",
+		"3939393939393939", "3939393939393939", "E22B19A55086774B",
+		"3A3A3A3A3A3A3A3A", "3A3A3A3A3A3A3A3A", "B04A2AAC925ABB0B",
+		"3B3B3B3B3B3B3B3B", "3B3B3B3B3B3B3B3B", "8D250D58361597FC",
+		"3C3C3C3C3C3C3C3C", "3C3C3C3C3C3C3C3C", "51F0114FB6A6CD37",
+		"3D3D3D3D3D3D3D3D", "3D3D3D3D3D3D3D3D", "9D0BB4DB830ECB73",
+		"3E3E3E3E3E3E3E3E", "3E3E3E3E3E3E3E3E", "E96089D6368F3E1A",
+		"3F3F3F3F3F3F3F3F", "3F3F3F3F3F3F3F3F", "5C4CA877A4E1E92D",
+		"4040404040404040", "4040404040404040", "6D55DDBC8DEA95FF",
+		"4141414141414141", "4141414141414141", "19DF84AC95551003",
+		"4242424242424242", "4242424242424242", "724E7332696D08A7",
+		"4343434343434343", "4343434343434343", "B91810B8CDC58FE2",
+		"4444444444444444", "4444444444444444", "06E23526EDCCD0C4",
+		"4545454545454545", "4545454545454545", "EF52491D5468D441",
+		"4646464646464646", "4646464646464646", "48019C59E39B90C5",
+		"4747474747474747", "4747474747474747", "0544083FB902D8C0",
+		"4848484848484848", "4848484848484848", "63B15CADA668CE12",
+		"4949494949494949", "4949494949494949", "EACC0C1264171071",
+		"4A4A4A4A4A4A4A4A", "4A4A4A4A4A4A4A4A", "9D2B8C0AC605F274",
+		"4B4B4B4B4B4B4B4B", "4B4B4B4B4B4B4B4B", "C90F2F4C98A8FB2A",
+		"4C4C4C4C4C4C4C4C", "4C4C4C4C4C4C4C4C", "03481B4828FD1D04",
+		"4D4D4D4D4D4D4D4D", "4D4D4D4D4D4D4D4D", "C78FC45A1DCEA2E2",
+		"4E4E4E4E4E4E4E4E", "4E4E4E4E4E4E4E4E", "DB96D88C3460D801",
+		"4F4F4F4F4F4F4F4F", "4F4F4F4F4F4F4F4F", "6C69E720F5105518",
+		"5050505050505050", "5050505050505050", "0D262E418BC893F3",
+		"5151515151515151", "5151515151515151", "6AD84FD7848A0A5C",
+		"5252525252525252", "5252525252525252", "C365CB35B34B6114",
+		"5353535353535353", "5353535353535353", "1155392E877F42A9",
+		"5454545454545454", "5454545454545454", "531BE5F9405DA715",
+		"5555555555555555", "5555555555555555", "3BCDD41E6165A5E8",
+		"5656565656565656", "5656565656565656", "2B1FF5610A19270C",
+		"5757575757575757", "5757575757575757", "D90772CF3F047CFD",
+		"5858585858585858", "5858585858585858", "1BEA27FFB72457B7",
+		"5959595959595959", "5959595959595959", "85C3E0C429F34C27",
+		"5A5A5A5A5A5A5A5A", "5A5A5A5A5A5A5A5A", "F9038021E37C7618",
+		"5B5B5B5B5B5B5B5B", "5B5B5B5B5B5B5B5B", "35BC6FF838DBA32F",
+		"5C5C5C5C5C5C5C5C", "5C5C5C5C5C5C5C5C", "4927ACC8CE45ECE7",
+		"5D5D5D5D5D5D5D5D", "5D5D5D5D5D5D5D5D", "E812EE6E3572985C",
+		"5E5E5E5E5E5E5E5E", "5E5E5E5E5E5E5E5E", "9BB93A89627BF65F",
+		"5F5F5F5F5F5F5F5F", "5F5F5F5F5F5F5F5F", "EF12476884CB74CA",
+		"6060606060606060", "6060606060606060", "1BF17E00C09E7CBF",
+		"6161616161616161", "6161616161616161", "29932350C098DB5D",
+		"6262626262626262", "6262626262626262", "B476E6499842AC54",
+		"6363636363636363", "6363636363636363", "5C662C29C1E96056",
+		"6464646464646464", "6464646464646464", "3AF1703D76442789",
+		"6565656565656565", "6565656565656565", "86405D9B425A8C8C",
+		"6666666666666666", "6666666666666666", "EBBF4810619C2C55",
+		"6767676767676767", "6767676767676767", "F8D1CD7367B21B5D",
+		"6868686868686868", "6868686868686868", "9EE703142BF8D7E2",
+		"6969696969696969", "6969696969696969", "5FDFFFC3AAAB0CB3",
+		"6A6A6A6A6A6A6A6A", "6A6A6A6A6A6A6A6A", "26C940AB13574231",
+		"6B6B6B6B6B6B6B6B", "6B6B6B6B6B6B6B6B", "1E2DC77E36A84693",
+		"6C6C6C6C6C6C6C6C", "6C6C6C6C6C6C6C6C", "0F4FF4D9BC7E2244",
+		"6D6D6D6D6D6D6D6D", "6D6D6D6D6D6D6D6D", "A4C9A0D04D3280CD",
+		"6E6E6E6E6E6E6E6E", "6E6E6E6E6E6E6E6E", "9FAF2C96FE84919D",
+		"6F6F6F6F6F6F6F6F", "6F6F6F6F6F6F6F6F", "115DBC965E6096C8",
+		"7070707070707070", "7070707070707070", "AF531E9520994017",
+		"7171717171717171", "7171717171717171", "B971ADE70E5C89EE",
+		"7272727272727272", "7272727272727272", "415D81C86AF9C376",
+		"7373737373737373", "7373737373737373", "8DFB864FDB3C6811",
+		"7474747474747474", "7474747474747474", "10B1C170E3398F91",
+		"7575757575757575", "7575757575757575", "CFEF7A1C0218DB1E",
+		"7676767676767676", "7676767676767676", "DBAC30A2A40B1B9C",
+		"7777777777777777", "7777777777777777", "89D3BF37052162E9",
+		"7878787878787878", "7878787878787878", "80D9230BDAEB67DC",
+		"7979797979797979", "7979797979797979", "3440911019AD68D7",
+		"7A7A7A7A7A7A7A7A", "7A7A7A7A7A7A7A7A", "9626FE57596E199E",
+		"7B7B7B7B7B7B7B7B", "7B7B7B7B7B7B7B7B", "DEA0B796624BB5BA",
+		"7C7C7C7C7C7C7C7C", "7C7C7C7C7C7C7C7C", "E9E40542BDDB3E9D",
+		"7D7D7D7D7D7D7D7D", "7D7D7D7D7D7D7D7D", "8AD99914B354B911",
+		"7E7E7E7E7E7E7E7E", "7E7E7E7E7E7E7E7E", "6F85B98DD12CB13B",
+		"7F7F7F7F7F7F7F7F", "7F7F7F7F7F7F7F7F", "10130DA3C3A23924",
+		"8080808080808080", "8080808080808080", "EFECF25C3C5DC6DB",
+		"8181818181818181", "8181818181818181", "907A46722ED34EC4",
+		"8282828282828282", "8282828282828282", "752666EB4CAB46EE",
+		"8383838383838383", "8383838383838383", "161BFABD4224C162",
+		"8484848484848484", "8484848484848484", "215F48699DB44A45",
+		"8585858585858585", "8585858585858585", "69D901A8A691E661",
+		"8686868686868686", "8686868686868686", "CBBF6EEFE6529728",
+		"8787878787878787", "8787878787878787", "7F26DCF425149823",
+		"8888888888888888", "8888888888888888", "762C40C8FADE9D16",
+		"8989898989898989", "8989898989898989", "2453CF5D5BF4E463",
+		"8A8A8A8A8A8A8A8A", "8A8A8A8A8A8A8A8A", "301085E3FDE724E1",
+		"8B8B8B8B8B8B8B8B", "8B8B8B8B8B8B8B8B", "EF4E3E8F1CC6706E",
+		"8C8C8C8C8C8C8C8C", "8C8C8C8C8C8C8C8C", "720479B024C397EE",
+		"8D8D8D8D8D8D8D8D", "8D8D8D8D8D8D8D8D", "BEA27E3795063C89",
+		"8E8E8E8E8E8E8E8E", "8E8E8E8E8E8E8E8E", "468E5218F1A37611",
+		"8F8F8F8F8F8F8F8F", "8F8F8F8F8F8F8F8F", "50ACE16ADF66BFE8",
+		"9090909090909090", "9090909090909090", "EEA24369A19F6937",
+		"9191919191919191", "9191919191919191", "6050D369017B6E62",
+		"9292929292929292", "9292929292929292", "5B365F2FB2CD7F32",
+		"9393939393939393", "9393939393939393", "F0B00B264381DDBB",
+		"9494949494949494", "9494949494949494", "E1D23881C957B96C",
+		"9595959595959595", "9595959595959595", "D936BF54ECA8BDCE",
+		"9696969696969696", "9696969696969696", "A020003C5554F34C",
+		"9797979797979797", "9797979797979797", "6118FCEBD407281D",
+		"9898989898989898", "9898989898989898", "072E328C984DE4A2",
+		"9999999999999999", "9999999999999999", "1440B7EF9E63D3AA",
+		"9A9A9A9A9A9A9A9A", "9A9A9A9A9A9A9A9A", "79BFA264BDA57373",
+		"9B9B9B9B9B9B9B9B", "9B9B9B9B9B9B9B9B", "C50E8FC289BBD876",
+		"9C9C9C9C9C9C9C9C", "9C9C9C9C9C9C9C9C", "A399D3D63E169FA9",
+		"9D9D9D9D9D9D9D9D", "9D9D9D9D9D9D9D9D", "4B8919B667BD53AB",
+		"9E9E9E9E9E9E9E9E", "9E9E9E9E9E9E9E9E", "D66CDCAF3F6724A2",
+		"9F9F9F9F9F9F9F9F", "9F9F9F9F9F9F9F9F", "E40E81FF3F618340",
+		"A0A0A0A0A0A0A0A0", "A0A0A0A0A0A0A0A0", "10EDB8977B348B35",
+		"A1A1A1A1A1A1A1A1", "A1A1A1A1A1A1A1A1", "6446C5769D8409A0",
+		"A2A2A2A2A2A2A2A2", "A2A2A2A2A2A2A2A2", "17ED1191CA8D67A3",
+		"A3A3A3A3A3A3A3A3", "A3A3A3A3A3A3A3A3", "B6D8533731BA1318",
+		"A4A4A4A4A4A4A4A4", "A4A4A4A4A4A4A4A4", "CA439007C7245CD0",
+		"A5A5A5A5A5A5A5A5", "A5A5A5A5A5A5A5A5", "06FC7FDE1C8389E7",
+		"A6A6A6A6A6A6A6A6", "A6A6A6A6A6A6A6A6", "7A3C1F3BD60CB3D8",
+		"A7A7A7A7A7A7A7A7", "A7A7A7A7A7A7A7A7", "E415D80048DBA848",
+		"A8A8A8A8A8A8A8A8", "A8A8A8A8A8A8A8A8", "26F88D30C0FB8302",
+		"A9A9A9A9A9A9A9A9", "A9A9A9A9A9A9A9A9", "D4E00A9EF5E6D8F3",
+		"AAAAAAAAAAAAAAAA", "AAAAAAAAAAAAAAAA", "C4322BE19E9A5A17",
+		"ABABABABABABABAB", "ABABABABABABABAB", "ACE41A06BFA258EA",
+		"ACACACACACACACAC", "ACACACACACACACAC", "EEAAC6D17880BD56",
+		"ADADADADADADADAD", "ADADADADADADADAD", "3C9A34CA4CB49EEB",
+		"AEAEAEAEAEAEAEAE", "AEAEAEAEAEAEAEAE", "9527B0287B75F5A3",
+		"AFAFAFAFAFAFAFAF", "AFAFAFAFAFAFAFAF", "F2D9D1BE74376C0C",
+		"B0B0B0B0B0B0B0B0", "B0B0B0B0B0B0B0B0", "939618DF0AEFAAE7",
+		"B1B1B1B1B1B1B1B1", "B1B1B1B1B1B1B1B1", "24692773CB9F27FE",
+		"B2B2B2B2B2B2B2B2", "B2B2B2B2B2B2B2B2", "38703BA5E2315D1D",
+		"B3B3B3B3B3B3B3B3", "B3B3B3B3B3B3B3B3", "FCB7E4B7D702E2FB",
+		"B4B4B4B4B4B4B4B4", "B4B4B4B4B4B4B4B4", "36F0D0B3675704D5",
+		"B5B5B5B5B5B5B5B5", "B5B5B5B5B5B5B5B5", "62D473F539FA0D8B",
+		"B6B6B6B6B6B6B6B6", "B6B6B6B6B6B6B6B6", "1533F3ED9BE8EF8E",
+		"B7B7B7B7B7B7B7B7", "B7B7B7B7B7B7B7B7", "9C4EA352599731ED",
+		"B8B8B8B8B8B8B8B8", "B8B8B8B8B8B8B8B8", "FABBF7C046FD273F",
+		"B9B9B9B9B9B9B9B9", "B9B9B9B9B9B9B9B9", "B7FE63A61C646F3A",
+		"BABABABABABABABA", "BABABABABABABABA", "10ADB6E2AB972BBE",
+		"BBBBBBBBBBBBBBBB", "BBBBBBBBBBBBBBBB", "F91DCAD912332F3B",
+		"BCBCBCBCBCBCBCBC", "BCBCBCBCBCBCBCBC", "46E7EF47323A701D",
+		"BDBDBDBDBDBDBDBD", "BDBDBDBDBDBDBDBD", "8DB18CCD9692F758",
+		"BEBEBEBEBEBEBEBE", "BEBEBEBEBEBEBEBE", "E6207B536AAAEFFC",
+		"BFBFBFBFBFBFBFBF", "BFBFBFBFBFBFBFBF", "92AA224372156A00",
+		"C0C0C0C0C0C0C0C0", "C0C0C0C0C0C0C0C0", "A3B357885B1E16D2",
+		"C1C1C1C1C1C1C1C1", "C1C1C1C1C1C1C1C1", "169F7629C970C1E5",
+		"C2C2C2C2C2C2C2C2", "C2C2C2C2C2C2C2C2", "62F44B247CF1348C",
+		"C3C3C3C3C3C3C3C3", "C3C3C3C3C3C3C3C3", "AE0FEEB0495932C8",
+		"C4C4C4C4C4C4C4C4", "C4C4C4C4C4C4C4C4", "72DAF2A7C9EA6803",
+		"C5C5C5C5C5C5C5C5", "C5C5C5C5C5C5C5C5", "4FB5D5536DA544F4",
+		"C6C6C6C6C6C6C6C6", "C6C6C6C6C6C6C6C6", "1DD4E65AAF7988B4",
+		"C7C7C7C7C7C7C7C7", "C7C7C7C7C7C7C7C7", "76BF084C1535A6C6",
+		"C8C8C8C8C8C8C8C8", "C8C8C8C8C8C8C8C8", "AFEC35B09D36315F",
+		"C9C9C9C9C9C9C9C9", "C9C9C9C9C9C9C9C9", "C8078A6148818403",
+		"CACACACACACACACA", "CACACACACACACACA", "4DA91CB4124B67FE",
+		"CBCBCBCBCBCBCBCB", "CBCBCBCBCBCBCBCB", "2DABFEB346794C3D",
+		"CCCCCCCCCCCCCCCC", "CCCCCCCCCCCCCCCC", "FBCD12C790D21CD7",
+		"CDCDCDCDCDCDCDCD", "CDCDCDCDCDCDCDCD", "536873DB879CC770",
+		"CECECECECECECECE", "CECECECECECECECE", "9AA159D7309DA7A0",
+		"CFCFCFCFCFCFCFCF", "CFCFCFCFCFCFCFCF", "0B844B9D8C4EA14A",
+		"D0D0D0D0D0D0D0D0", "D0D0D0D0D0D0D0D0", "3BBD84CE539E68C4",
+		"D1D1D1D1D1D1D1D1", "D1D1D1D1D1D1D1D1", "CF3E4F3E026E2C8E",
+		"D2D2D2D2D2D2D2D2", "D2D2D2D2D2D2D2D2", "82F85885D542AF58",
+		"D3D3D3D3D3D3D3D3", "D3D3D3D3D3D3D3D3", "22D334D6493B3CB6",
+		"D4D4D4D4D4D4D4D4", "D4D4D4D4D4D4D4D4", "47E9CB3E3154D673",
+		"D5D5D5D5D5D5D5D5", "D5D5D5D5D5D5D5D5", "2352BCC708ADC7E9",
+		"D6D6D6D6D6D6D6D6", "D6D6D6D6D6D6D6D6", "8C0F3BA0C8601980",
+		"D7D7D7D7D7D7D7D7", "D7D7D7D7D7D7D7D7", "EE5E9FD70CEF00E9",
+		"D8D8D8D8D8D8D8D8", "D8D8D8D8D8D8D8D8", "DEF6BDA6CABF9547",
+		"D9D9D9D9D9D9D9D9", "D9D9D9D9D9D9D9D9", "4DADD04A0EA70F20",
+		"DADADADADADADADA", "DADADADADADADADA", "C1AA16689EE1B482",
+		"DBDBDBDBDBDBDBDB", "DBDBDBDBDBDBDBDB", "F45FC26193E69AEE",
+		"DCDCDCDCDCDCDCDC", "DCDCDCDCDCDCDCDC", "D0CFBB937CEDBFB5",
+		"DDDDDDDDDDDDDDDD", "DDDDDDDDDDDDDDDD", "F0752004EE23D87B",
+		"DEDEDEDEDEDEDEDE", "DEDEDEDEDEDEDEDE", "77A791E28AA464A5",
+		"DFDFDFDFDFDFDFDF", "DFDFDFDFDFDFDFDF", "E7562A7F56FF4966",
+		"E0E0E0E0E0E0E0E0", "E0E0E0E0E0E0E0E0", "B026913F2CCFB109",
+		"E1E1E1E1E1E1E1E1", "E1E1E1E1E1E1E1E1", "0DB572DDCE388AC7",
+		"E2E2E2E2E2E2E2E2", "E2E2E2E2E2E2E2E2", "D9FA6595F0C094CA",
+		"E3E3E3E3E3E3E3E3", "E3E3E3E3E3E3E3E3", "ADE4804C4BE4486E",
+		"E4E4E4E4E4E4E4E4", "E4E4E4E4E4E4E4E4", "007B81F520E6D7DA",
+		"E5E5E5E5E5E5E5E5", "E5E5E5E5E5E5E5E5", "961AEB77BFC10B3C",
+		"E6E6E6E6E6E6E6E6", "E6E6E6E6E6E6E6E6", "8A8DD870C9B14AF2",
+		"E7E7E7E7E7E7E7E7", "E7E7E7E7E7E7E7E7", "3CC02E14B6349B25",
+		"E8E8E8E8E8E8E8E8", "E8E8E8E8E8E8E8E8", "BAD3EE68BDDB9607",
+		"E9E9E9E9E9E9E9E9", "E9E9E9E9E9E9E9E9", "DFF918E93BDAD292",
+		"EAEAEAEAEAEAEAEA", "EAEAEAEAEAEAEAEA", "8FE559C7CD6FA56D",
+		"EBEBEBEBEBEBEBEB", "EBEBEBEBEBEBEBEB", "C88480835C1A444C",
+		"ECECECECECECECEC", "ECECECECECECECEC", "D6EE30A16B2CC01E",
+		"EDEDEDEDEDEDEDED", "EDEDEDEDEDEDEDED", "6932D887B2EA9C1A",
+		"EEEEEEEEEEEEEEEE", "EEEEEEEEEEEEEEEE", "0BFC865461F13ACC",
+		"EFEFEFEFEFEFEFEF", "EFEFEFEFEFEFEFEF", "228AEA0D403E807A",
+		"F0F0F0F0F0F0F0F0", "F0F0F0F0F0F0F0F0", "2A2891F65BB8173C",
+		"F1F1F1F1F1F1F1F1", "F1F1F1F1F1F1F1F1", "5D1B8FAF7839494B",
+		"F2F2F2F2F2F2F2F2", "F2F2F2F2F2F2F2F2", "1C0A9280EECF5D48",
+		"F3F3F3F3F3F3F3F3", "F3F3F3F3F3F3F3F3", "6CBCE951BBC30F74",
+		"F4F4F4F4F4F4F4F4", "F4F4F4F4F4F4F4F4", "9CA66E96BD08BC70",
+		"F5F5F5F5F5F5F5F5", "F5F5F5F5F5F5F5F5", "F5D779FCFBB28BF3",
+		"F6F6F6F6F6F6F6F6", "F6F6F6F6F6F6F6F6", "0FEC6BBF9B859184",
+		"F7F7F7F7F7F7F7F7", "F7F7F7F7F7F7F7F7", "EF88D2BF052DBDA8",
+		"F8F8F8F8F8F8F8F8", "F8F8F8F8F8F8F8F8", "39ADBDDB7363090D",
+		"F9F9F9F9F9F9F9F9", "F9F9F9F9F9F9F9F9", "C0AEAF445F7E2A7A",
+		"FAFAFAFAFAFAFAFA", "FAFAFAFAFAFAFAFA", "C66F54067298D4E9",
+		"FBFBFBFBFBFBFBFB", "FBFBFBFBFBFBFBFB", "E0BA8F4488AAF97C",
+		"FCFCFCFCFCFCFCFC", "FCFCFCFCFCFCFCFC", "67B36E2875D9631C",
+		"FDFDFDFDFDFDFDFD", "FDFDFDFDFDFDFDFD", "1ED83D49E267191D",
+		"FEFEFEFEFEFEFEFE", "FEFEFEFEFEFEFEFE", "66B2B23EA84693AD",
+		"FFFFFFFFFFFFFFFF", "FFFFFFFFFFFFFFFF", "7359B2163E4EDC58",
+		"0001020304050607", "0011223344556677", "3EF0A891CF8ED990",
+		"2BD6459F82C5B300", "EA024714AD5C4D84", "126EFE8ED312190A"
+	};
+
+	/*
+	 * Known-answer tests for DES/3DES in CBC mode.
+	 * Order: key, IV, plaintext, ciphertext.
+	 */
+	static string[] KAT_DES_CBC = {
+		/*
+		 * From NIST validation suite (tdesmmt.zip).
+		 */
+		"34a41a8c293176c1b30732ecfe38ae8a34a41a8c293176c1",
+		"f55b4855228bd0b4",
+		"7dd880d2a9ab411c",
+		"c91892948b6cadb4",
+
+		"70a88fa1dfb9942fa77f40157ffef2ad70a88fa1dfb9942f",
+		"ece08ce2fdc6ce80",
+		"bc225304d5a3a5c9918fc5006cbc40cc",
+		"27f67dc87af7ddb4b68f63fa7c2d454a",
+
+		"e091790be55be0bc0780153861a84adce091790be55be0bc",
+		"fd7d430f86fbbffe",
+		"03c7fffd7f36499c703dedc9df4de4a92dd4382e576d6ae9",
+		"053aeba85dd3a23bfbe8440a432f9578f312be60fb9f0035",
+
+		"857feacd16157c58e5347a70e56e578a857feacd16157c58",
+		"002dcb6d46ef0969",
+		"1f13701c7f0d7385307507a18e89843ebd295bd5e239ef109347a6898c6d3fd5",
+		"a0e4edde34f05bd8397ce279e49853e9387ba04be562f5fa19c3289c3f5a3391",
+
+		"a173545b265875ba852331fbb95b49a8a173545b265875ba",
+		"ab385756391d364c",
+		"d08894c565608d9ae51dda63b85b3b33b1703bb5e4f1abcbb8794e743da5d6f3bf630f2e9b6d5b54",
+		"370b47acf89ac6bdbb13c9a7336787dc41e1ad8beead32281d0609fb54968404bdf2894892590658",
+
+		"26376bcb2f23df1083cd684fe00ed3c726376bcb2f23df10",
+		"33acfb0f3d240ea6",
+		"903a1911da1e6877f23c1985a9b61786ef438e0ce1240885035ad60fc916b18e5d71a1fb9c5d1eff61db75c0076f6efb",
+		"7a4f7510f6ec0b93e2495d21a8355684d303a770ebda2e0e51ff33d72b20cb73e58e2e3de2ef6b2e12c504c0f181ba63",
+
+		"3e1f98135d027cec752f67765408a7913e1f98135d027cec",
+		"11f5f2304b28f68b",
+		"7c022f5af24f7925d323d4d0e20a2ce49272c5e764b22c806f4b6ddc406d864fe5bd1c3f45556d3eb30c8676c2f8b54a5a32423a0bd95a07",
+		"2bb4b131fa4ae0b4f0378a2cdb68556af6eee837613016d7ea936f3931f25f8b3ae351d5e9d00be665676e2400408b5db9892d95421e7f1a",
+
+		"13b9d549cd136ec7bf9e9810ef2cdcbf13b9d549cd136ec7",
+		"a82c1b1057badcc8",
+		"1fff1563bc1645b55cb23ea34a0049dfc06607150614b621dedcb07f20433402a2d869c95ac4a070c7a3da838c928a385f899c5d21ecb58f4e5cbdad98d39b8c",
+		"75f804d4a2c542a31703e23df26cc38861a0729090e6eae5672c1db8c0b09fba9b125bbca7d6c7d330b3859e6725c6d26de21c4e3af7f5ea94df3cde2349ce37",
+
+		"20320dfdad579bb57c6e4acd769dbadf20320dfdad579bb5",
+		"879201b5857ccdea",
+		"0431283cc8bb4dc7750a9d5c68578486932091632a12d0a79f2c54e3d122130881fff727050f317a40fcd1a8d13793458b99fc98254ba6a233e3d95b55cf5a3faff78809999ea4bf",
+		"85d17840eb2af5fc727027336bfd71a2b31bd14a1d9eb64f8a08bfc4f56eaa9ca7654a5ae698287869cc27324813730de4f1384e0b8cfbc472ff5470e3c5e4bd8ceb23dc2d91988c",
+
+		"23abb073a2df34cb3d1fdce6b092582c23abb073a2df34cb",
+		"7d7fbf19e8562d32",
+		"31e718fd95e6d7ca4f94763191add2674ab07c909d88c486916c16d60a048a0cf8cdb631cebec791362cd0c202eb61e166b65c1f65d0047c8aec57d3d84b9e17032442dce148e1191b06a12c284cc41e",
+		"c9a3f75ab6a7cd08a7fd53ca540aafe731d257ee1c379fadcc4cc1a06e7c12bddbeb7562c436d1da849ed072629e82a97b56d9becc25ff4f16f21c5f2a01911604f0b5c49df96cb641faee662ca8aa68",
+
+		"b5cb1504802326c73df186e3e352a20de643b0d63ee30e37",
+		"43f791134c5647ba",
+		"dcc153cef81d6f24",
+		"92538bd8af18d3ba",
+
+		"a49d7564199e97cb529d2c9d97bf2f98d35edf57ba1f7358",
+		"c2e999cb6249023c",
+		"c689aee38a301bb316da75db36f110b5",
+		"e9afaba5ec75ea1bbe65506655bb4ecb",
+
+		"1a5d4c0825072a15a8ad9dfdaeda8c048adffb85bc4fced0",
+		"7fcfa736f7548b6f",
+		"983c3edacd939406010e1bc6ff9e12320ac5008117fa8f84",
+		"d84fa24f38cf451ca2c9adc960120bd8ff9871584fe31cee",
+
+		"d98aadc76d4a3716158c32866efbb9ce834af2297379a49d",
+		"3c5220327c502b44",
+		"6174079dda53ca723ebf00a66837f8d5ce648c08acaa5ee45ffe62210ef79d3e",
+		"f5bd4d600bed77bec78409e3530ebda1d815506ed53103015b87e371ae000958",
+
+		"ef6d3e54266d978ffb0b8ce6689d803e2cd34cc802fd0252",
+		"38bae5bce06d0ad9",
+		"c4f228b537223cd01c0debb5d9d4e12ba71656618d119b2f8f0af29d23efa3a9e43c4c458a1b79a0",
+		"9e3289fb18379f55aa4e45a7e0e6df160b33b75f8627ad0954f8fdcb78cee55a4664caeda1000fe5",
+
+		"625bc19b19df83abfb2f5bec9d4f2062017525a75bc26e70",
+		"bd0cff364ff69a91",
+		"8152d2ab876c3c8201403a5a406d3feaf27319dbea6ad01e24f4d18203704b86de70da6bbb6d638e5aba3ff576b79b28",
+		"706fe7a973fac40e25b2b4499ce527078944c70e976d017b6af86a3a7a6b52943a72ba18a58000d2b61fdc3bfef2bc4a",
+
+		"b6383176046e6880a1023bf45768b5bf5119022fe054bfe5",
+		"ec13ca541c43401e",
+		"cd5a886e9af011346c4dba36a424f96a78a1ddf28aaa4188bf65451f4efaffc7179a6dd237c0ae35d9b672314e5cb032612597f7e462c6f3",
+		"b030f976f46277ee211c4a324d5c87555d1084513a1223d3b84416b52bbc28f4b77f3a9d8d0d91dc37d3dbe8af8be98f74674b02f9a38527",
+
+		"3d8cf273d343b9aedccddacb91ad86206737adc86b4a49a7",
+		"bb3a9a0c71c62ef0",
+		"1fde3991c32ce220b5b6666a9234f2fd7bd24b921829fd9cdc6eb4218be9eac9faa9c2351777349128086b6d58776bc86ff2f76ee1b3b2850a318462b8983fa1",
+		"422ce705a46bb52ad928dab6c863166d617c6fc24003633120d91918314bbf464cea7345c3c35f2042f2d6929735d74d7728f22fea618a0b9cf5b1281acb13fb",
+
+		"fbceb5cb646b925be0b92f7f6b493d5e5b16e9159732732a",
+		"2e17b3c7025ae86b",
+		"4c309bc8e1e464fdd2a2b8978645d668d455f7526bd8d7b6716a722f6a900b815c4a73cc30e788065c1dfca7bf5958a6cc5440a5ebe7f8691c20278cde95db764ff8ce8994ece89c",
+		"c02129bdf4bbbd75e71605a00b12c80db6b4e05308e916615011f09147ed915dd1bc67f27f9e027e4e13df36b55464a31c11b4d1fe3d855d89df492e1a7201b995c1ba16a8dbabee",
+
+		"9b162a0df8ad9b61c88676e3d586434570b902f12a2046e0",
+		"ebd6fefe029ad54b",
+		"f4c1c918e77355c8156f0fd778da52bff121ae5f2f44eaf4d2754946d0e10d1f18ce3a0176e69c18b7d20b6e0d0bee5eb5edfe4bd60e4d92adcd86bce72e76f94ee5cbcaa8b01cfddcea2ade575e66ac",
+		"1ff3c8709f403a8eff291aedf50c010df5c5ff64a8b205f1fce68564798897a390db16ee0d053856b75898009731da290fcc119dad987277aacef694872e880c4bb41471063fae05c89f25e4bd0cad6a"
+	};
+
+	/*
+	 * From RFC 7539. Each vector consists in 5 values:
+	 *    key (hex)
+	 *    iv (hex)
+	 *    counter (decimal)
+	 *    plain (hex)
+	 *    cipher (hex)
+	 */
+	static string[] KAT_CHACHA20 = {
+		"0000000000000000000000000000000000000000000000000000000000000000",
+		"000000000000000000000000",
+		"0",
+		"00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+		"76b8e0ada0f13d90405d6ae55386bd28bdd219b8a08ded1aa836efcc8b770dc7da41597c5157488d7724e03fb8d84a376a43b8f41518a11cc387b669b2ee6586",
+
+		"0000000000000000000000000000000000000000000000000000000000000001",
+		"000000000000000000000002",
+		"1",
+		"416e79207375626d697373696f6e20746f20746865204945544620696e74656e6465642062792074686520436f6e7472696275746f7220666f72207075626c69636174696f6e20617320616c6c206f722070617274206f6620616e204945544620496e7465726e65742d4472616674206f722052464320616e6420616e792073746174656d656e74206d6164652077697468696e2074686520636f6e74657874206f6620616e204945544620616374697669747920697320636f6e7369646572656420616e20224945544620436f6e747269627574696f6e222e20537563682073746174656d656e747320696e636c756465206f72616c2073746174656d656e747320696e20494554462073657373696f6e732c2061732077656c6c206173207772697474656e20616e6420656c656374726f6e696320636f6d6d756e69636174696f6e73206d61646520617420616e792074696d65206f7220706c6163652c207768696368206172652061646472657373656420746f",
+		"a3fbf07df3fa2fde4f376ca23e82737041605d9f4f4f57bd8cff2c1d4b7955ec2a97948bd3722915c8f3d337f7d370050e9e96d647b7c39f56e031ca5eb6250d4042e02785ececfa4b4bb5e8ead0440e20b6e8db09d881a7c6132f420e52795042bdfa7773d8a9051447b3291ce1411c680465552aa6c405b7764d5e87bea85ad00f8449ed8f72d0d662ab052691ca66424bc86d2df80ea41f43abf937d3259dc4b2d0dfb48a6c9139ddd7f76966e928e635553ba76c5c879d7b35d49eb2e62b0871cdac638939e25e8a1e0ef9d5280fa8ca328b351c3c765989cbcf3daa8b6ccc3aaf9f3979c92b3720fc88dc95ed84a1be059c6499b9fda236e7e818b04b0bc39c1e876b193bfe5569753f88128cc08aaa9b63d1a16f80ef2554d7189c411f5869ca52c5b83fa36ff216b9c1d30062bebcfd2dc5bce0911934fda79a86f6e698ced759c3ff9b6477338f3da4f9cd8514ea9982ccafb341b2384dd902f3d1ab7ac61dd29c6f21ba5b862f3730e37cfdc4fd806c22f221",
+
+		"1c9240a5eb55d38af333888604f6b5f0473917c1402b80099dca5cbc207075c0",
+		"000000000000000000000002",
+		"42",
+		"2754776173206272696c6c69672c20616e642074686520736c6974687920746f7665730a446964206779726520616e642067696d626c6520696e2074686520776162653a0a416c6c206d696d737920776572652074686520626f726f676f7665732c0a416e6420746865206d6f6d65207261746873206f757467726162652e",
+		"62e6347f95ed87a45ffae7426f27a1df5fb69110044c0d73118effa95b01e5cf166d3df2d721caf9b21e5fb14c616871fd84c54f9d65b283196c7fe4f60553ebf39c6402c42234e32a356b3e764312a61a5532055716ead6962568f87d3f3f7704c6a8d1bcd1bf4d50d6154b6da731b187b58dfd728afa36757a797ac188d1"
+	};
+
+	/*
+	 * From: http://csrc.nist.gov/groups/ST/toolkit/BCM/documents/proposedmodes/gcm/gcm-revised-spec.pdf
+	 */
+	static string[] KAT_GHASH = {
+
+		"66e94bd4ef8a2c3b884cfa59ca342b2e",
+		"",
+		"",
+		"00000000000000000000000000000000",
+
+		"66e94bd4ef8a2c3b884cfa59ca342b2e",
+		"",
+		"0388dace60b6a392f328c2b971b2fe78",
+		"f38cbb1ad69223dcc3457ae5b6b0f885",
+
+		"b83b533708bf535d0aa6e52980d53b78",
+		"",
+		"42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091473f5985",
+		"7f1b32b81b820d02614f8895ac1d4eac",
+
+		"b83b533708bf535d0aa6e52980d53b78",
+		"feedfacedeadbeeffeedfacedeadbeefabaddad2",
+		"42831ec2217774244b7221b784d0d49ce3aa212f2c02a4e035c17e2329aca12e21d514b25466931c7d8f6a5aac84aa051ba30b396a0aac973d58e091",
+		"698e57f70e6ecc7fd9463b7260a9ae5f",
+
+		"b83b533708bf535d0aa6e52980d53b78",
+		"feedfacedeadbeeffeedfacedeadbeefabaddad2",
+		"61353b4c2806934a777ff51fa22a4755699b2a714fcdc6f83766e5f97b6c742373806900e49f24b22b097544d4896b424989b5e1ebac0f07c23f4598",
+		"df586bb4c249b92cb6922877e444d37b",
+
+		"b83b533708bf535d0aa6e52980d53b78",
+		"feedfacedeadbeeffeedfacedeadbeefabaddad2",
+		"8ce24998625615b603a033aca13fb894be9112a5c3a211a8ba262a3cca7e2ca701e4a9a4fba43c90ccdcb281d48c7c6fd62875d2aca417034c34aee5",
+		"1c5afe9760d3932f3c9a878aac3dc3de",
+
+		"aae06992acbf52a3e8f4a96ec9300bd7",
+		"",
+		"98e7247c07f0fe411c267e4384b0f600",
+		"e2c63f0ac44ad0e02efa05ab6743d4ce",
+
+		"466923ec9ae682214f2c082badb39249",
+		"",
+		"3980ca0b3c00e841eb06fac4872a2757859e1ceaa6efd984628593b40ca1e19c7d773d00c144c525ac619d18c84a3f4718e2448b2fe324d9ccda2710acade256",
+		"51110d40f6c8fff0eb1ae33445a889f0",
+
+		"466923ec9ae682214f2c082badb39249",
+		"feedfacedeadbeeffeedfacedeadbeefabaddad2",
+		"3980ca0b3c00e841eb06fac4872a2757859e1ceaa6efd984628593b40ca1e19c7d773d00c144c525ac619d18c84a3f4718e2448b2fe324d9ccda2710",
+		"ed2ce3062e4a8ec06db8b4c490e8a268",
+
+		"466923ec9ae682214f2c082badb39249",
+		"feedfacedeadbeeffeedfacedeadbeefabaddad2",
+		"0f10f599ae14a154ed24b36e25324db8c566632ef2bbb34f8347280fc4507057fddc29df9a471f75c66541d4d4dad1c9e93a19a58e8b473fa0f062f7",
+		"1e6a133806607858ee80eaf237064089",
+
+		"466923ec9ae682214f2c082badb39249",
+		"feedfacedeadbeeffeedfacedeadbeefabaddad2",
+		"d27e88681ce3243c4830165a8fdcf9ff1de9a1d8e6b447ef6ef7b79828666e4581e79012af34ddd9e2f037589b292db3e67c036745fa22e7e9b7373b",
+		"82567fb0b4cc371801eadec005968e94",
+
+		"dc95c078a2408989ad48a21492842087",
+		"",
+		"cea7403d4d606b6e074ec5d3baf39d18",
+		"83de425c5edc5d498f382c441041ca92",
+
+		"acbef20579b4b8ebce889bac8732dad7",
+		"",
+		"522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662898015ad",
+		"4db870d37cb75fcb46097c36230d1612",
+
+		"acbef20579b4b8ebce889bac8732dad7",
+		"feedfacedeadbeeffeedfacedeadbeefabaddad2",
+		"522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662",
+		"8bd0c4d8aacd391e67cca447e8c38f65",
+
+		"acbef20579b4b8ebce889bac8732dad7",
+		"feedfacedeadbeeffeedfacedeadbeefabaddad2",
+		"c3762df1ca787d32ae47c13bf19844cbaf1ae14d0b976afac52ff7d79bba9de0feb582d33934a4f0954cc2363bc73f7862ac430e64abe499f47c9b1f",
+		"75a34288b8c68f811c52b2e9a2f97f63",
+
+		"acbef20579b4b8ebce889bac8732dad7",
+		"feedfacedeadbeeffeedfacedeadbeefabaddad2",
+		"5a8def2f0c9e53f1f75d7853659e2a20eeb2b22aafde6419a058ab4f6f746bf40fc0c3b780f244452da3ebf1c5d82cdea2418997200ef82e44ae7e3f",
+		"d5ffcf6fc5ac4d69722187421a7f170b"
+	};
+
+	static string[] ECDSA_K_P256 = {
+  "04"
++ "60FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB6"
++ "7903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299",
+
+  "C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721"
+	};
+
+	static string[] ECDSA_SIGS_P256 = {
+  "61340C88C3AAEBEB4F6D667F672CA9759A6CCAA9FA8811313039EE4A35471D32",
+  "6D7F147DAC089441BB2E2FE8F7A3FA264B9C475098FDCF6E00D7C996E1B8B7EB",
+
+  "53B2FFF5D1752B2C689DF257C04C40A587FABABB3F6FC2702F1343AF7CA9AA3F",
+  "B9AFB64FDC03DC1A131C7D2386D11E349F070AA432A4ACC918BEA988BF75C74C",
+
+  "EFD48B2AACB6A8FD1140DD9CD45E81D69D2C877B56AAF991C34D0EA84EAF3716",
+  "F7CB1C942D657C41D436C7A1B6E29F65F3E900DBB9AFF4064DC4AB2F843ACDA8",
+
+  "0EAFEA039B20E9B42309FB1D89E213057CBF973DC0CFC8F129EDDDC800EF7719",
+  "4861F0491E6998B9455193E34E7B0D284DDD7149A74B95B9261F13ABDE940954",
+
+  "8496A60B5E9B47C825488827E0495B0E3FA109EC4568FD3F8D1097678EB97F00",
+  "2362AB1ADBE2B8ADF9CB9EDAB740EA6049C028114F2460F96554F61FAE3302FE",
+
+  "0CBCC86FD6ABD1D99E703E1EC50069EE5C0B4BA4B9AC60E409E8EC5910D81A89",
+  "01B9D7B73DFAA60D5651EC4591A0136F87653E0FD780C3B1BC872FFDEAE479B1",
+
+  "C37EDB6F0AE79D47C3C27E962FA269BB4F441770357E114EE511F662EC34A692",
+  "C820053A05791E521FCAAD6042D40AEA1D6B1A540138558F47D0719800E18F2D",
+
+  "F1ABB023518351CD71D881567B1EA663ED3EFCF6C5132B354F28D3B0B7D38367",
+  "019F4113742A2B14BD25926B49C649155F267E60D3814B4C0CC84250E46F0083",
+
+  "83910E8B48BB0C74244EBDF7F07A1C5413D61472BD941EF3920E623FBCCEBEB6",
+  "8DDBEC54CF8CD5874883841D712142A56A8D0F218F5003CB0296B6B509619F2C",
+
+  "461D93F31B6540894788FD206C07CFA0CC35F46FA3C91816FFF1040AD1581A04",
+  "39AF9F15DE0DB8D97E72719C74820D304CE5226E32DEDAE67519E840D1194E55"
+	};
+
+	static string[] ECDSA_K_P384 = {
+		"04"
+		+ "EC3A4E415B4E19A4568618029F427FA5DA9A8BC4AE92E02E"
+		+ "06AAE5286B300C64DEF8F0EA9055866064A254515480BC13"
+		+ "8015D9B72D7D57244EA8EF9AC0C621896708A59367F9DFB9"
+		+ "F54CA84B3F1C9DB1288B231C3AE0D4FE7344FD2533264720",
+
+		  "6B9D3DAD2E1B8C1C05B19875B6659F4DE23C3B667BF297BA"
+		+ "9AA47740787137D896D5724E4C70A825F872C9EA60D2EDF5"
+	};
+
+	static string[] ECDSA_SIGS_P384 = {
+		  "EC748D839243D6FBEF4FC5C4859A7DFFD7F3ABDDF7201454"
+		+ "0C16D73309834FA37B9BA002899F6FDA3A4A9386790D4EB2",
+		  "A3BCFA947BEEF4732BF247AC17F71676CB31A847B9FF0CBC"
+		+ "9C9ED4C1A5B3FACF26F49CA031D4857570CCB5CA4424A443",
+
+		  "42356E76B55A6D9B4631C865445DBE54E056D3B3431766D0"
+		+ "509244793C3F9366450F76EE3DE43F5A125333A6BE060122",
+		  "9DA0C81787064021E78DF658F2FBB0B042BF304665DB721F"
+		+ "077A4298B095E4834C082C03D83028EFBF93A3C23940CA8D",
+
+		  "21B13D1E013C7FA1392D03C5F99AF8B30C570C6F98D4EA8E"
+		+ "354B63A21D3DAA33BDE1E888E63355D92FA2B3C36D8FB2CD",
+		  "F3AA443FB107745BF4BD77CB3891674632068A10CA67E3D4"
+		+ "5DB2266FA7D1FEEBEFDC63ECCD1AC42EC0CB8668A4FA0AB0",
+
+		  "94EDBB92A5ECB8AAD4736E56C691916B3F88140666CE9FA7"
+		+ "3D64C4EA95AD133C81A648152E44ACF96E36DD1E80FABE46",
+		  "99EF4AEB15F178CEA1FE40DB2603138F130E740A19624526"
+		+ "203B6351D0A3A94FA329C145786E679E7B82C71A38628AC8",
+
+		  "ED0959D5880AB2D869AE7F6C2915C6D60F96507F9CB3E047"
+		+ "C0046861DA4A799CFE30F35CC900056D7C99CD7882433709",
+		  "512C8CCEEE3890A84058CE1E22DBC2198F42323CE8ACA913"
+		+ "5329F03C068E5112DC7CC3EF3446DEFCEB01A45C2667FDD5",
+
+		  "4BC35D3A50EF4E30576F58CD96CE6BF638025EE624004A1F"
+		+ "7789A8B8E43D0678ACD9D29876DAF46638645F7F404B11C7",
+		  "D5A6326C494ED3FF614703878961C0FDE7B2C278F9A65FD8"
+		+ "C4B7186201A2991695BA1C84541327E966FA7B50F7382282",
+
+		  "E8C9D0B6EA72A0E7837FEA1D14A1A9557F29FAA45D3E7EE8"
+		+ "88FC5BF954B5E62464A9A817C47FF78B8C11066B24080E72",
+		  "07041D4A7A0379AC7232FF72E6F77B6DDB8F09B16CCE0EC3"
+		+ "286B2BD43FA8C6141C53EA5ABEF0D8231077A04540A96B66",
+
+		  "6D6DEFAC9AB64DABAFE36C6BF510352A4CC27001263638E5"
+		+ "B16D9BB51D451559F918EEDAF2293BE5B475CC8F0188636B",
+		  "2D46F3BECBCC523D5F1A1256BF0C9B024D879BA9E838144C"
+		+ "8BA6BAEB4B53B47D51AB373F9845C0514EEFB14024787265",
+
+		  "8203B63D3C853E8D77227FB377BCF7B7B772E97892A80F36"
+		+ "AB775D509D7A5FEB0542A7F0812998DA8F1DD3CA3CF023DB",
+		  "DDD0760448D42D8A43AF45AF836FCE4DE8BE06B485E9B61B"
+		+ "827C2F13173923E06A739F040649A667BF3B828246BAA5A5",
+
+		  "A0D5D090C9980FAF3C2CE57B7AE951D31977DD11C775D314"
+		+ "AF55F76C676447D06FB6495CD21B4B6E340FC236584FB277",
+		  "976984E59B4C77B0E8E4460DCA3D9F20E07B9BB1F63BEEFA"
+		+ "F576F6B2E8B224634A2092CD3792E0159AD9CEE37659C736"
+	};
+
+	static string[] ECDSA_K_P521 = {
+  "04"
++ "01894550D0785932E00EAA23B694F213F8C3121F86DC97A04E5A7167DB4E5BCD37"
++ "1123D46E45DB6B5D5370A7F20FB633155D38FFA16D2BD761DCAC474B9A2F5023A4"
++ "00493101C962CD4D2FDDF782285E64584139C2F91B47F87FF82354D6630F746A28"
++ "A0DB25741B5B34A828008B22ACC23F924FAAFBD4D33F81EA66956DFEAA2BFDFCF5",
+
+  "00FAD06DAA62BA3B25D2FB40133DA757205DE67F5BB0018FEE8C86E1B68C7E75CA"
++ "A896EB32F1F47C70855836A6D16FCC1466F6D8FBEC67DB89EC0C08B0E996B83538"
+	};
+
+	static string[] ECDSA_SIGS_P521 = {
+  "00343B6EC45728975EA5CBA6659BBB6062A5FF89EEA58BE3C80B619F322C87910F"
++ "E092F7D45BB0F8EEE01ED3F20BABEC079D202AE677B243AB40B5431D497C55D75D",
+  "00E7B0E675A9B24413D448B8CC119D2BF7B2D2DF032741C096634D6D65D0DBE3D5"
++ "694625FB9E8104D3B842C1B0E2D0B98BEA19341E8676AEF66AE4EBA3D5475D5D16",
+
+  "01776331CFCDF927D666E032E00CF776187BC9FDD8E69D0DABB4109FFE1B5E2A30"
++ "715F4CC923A4A5E94D2503E9ACFED92857B7F31D7152E0F8C00C15FF3D87E2ED2E",
+  "0050CB5265417FE2320BBB5A122B8E1A32BD699089851128E360E620A30C7E17BA"
++ "41A666AF126CE100E5799B153B60528D5300D08489CA9178FB610A2006C254B41F",
+
+  "01511BB4D675114FE266FC4372B87682BAECC01D3CC62CF2303C92B3526012659D"
++ "16876E25C7C1E57648F23B73564D67F61C6F14D527D54972810421E7D87589E1A7",
+  "004A171143A83163D6DF460AAF61522695F207A58B95C0644D87E52AA1A347916E"
++ "4F7A72930B1BC06DBE22CE3F58264AFD23704CBB63B29B931F7DE6C9D949A7ECFC",
+
+  "01EA842A0E17D2DE4F92C15315C63DDF72685C18195C2BB95E572B9C5136CA4B4B"
++ "576AD712A52BE9730627D16054BA40CC0B8D3FF035B12AE75168397F5D50C67451",
+  "01F21A3CEE066E1961025FB048BD5FE2B7924D0CD797BABE0A83B66F1E35EEAF5F"
++ "DE143FA85DC394A7DEE766523393784484BDF3E00114A1C857CDE1AA203DB65D61",
+
+  "00C328FAFCBD79DD77850370C46325D987CB525569FB63C5D3BC53950E6D4C5F17"
++ "4E25A1EE9017B5D450606ADD152B534931D7D4E8455CC91F9B15BF05EC36E377FA",
+  "00617CCE7CF5064806C467F678D3B4080D6F1CC50AF26CA209417308281B68AF28"
++ "2623EAA63E5B5C0723D8B8C37FF0777B1A20F8CCB1DCCC43997F1EE0E44DA4A67A",
+
+  "013BAD9F29ABE20DE37EBEB823C252CA0F63361284015A3BF430A46AAA80B87B06"
++ "93F0694BD88AFE4E661FC33B094CD3B7963BED5A727ED8BD6A3A202ABE009D0367",
+  "01E9BB81FF7944CA409AD138DBBEE228E1AFCC0C890FC78EC8604639CB0DBDC90F"
++ "717A99EAD9D272855D00162EE9527567DD6A92CBD629805C0445282BBC916797FF",
+
+  "01C7ED902E123E6815546065A2C4AF977B22AA8EADDB68B2C1110E7EA44D42086B"
++ "FE4A34B67DDC0E17E96536E358219B23A706C6A6E16BA77B65E1C595D43CAE17FB",
+  "0177336676304FCB343CE028B38E7B4FBA76C1C1B277DA18CAD2A8478B2A9A9F5B"
++ "EC0F3BA04F35DB3E4263569EC6AADE8C92746E4C82F8299AE1B8F1739F8FD519A4",
+
+  "000E871C4A14F993C6C7369501900C4BC1E9C7B0B4BA44E04868B30B41D8071042"
++ "EB28C4C250411D0CE08CD197E4188EA4876F279F90B3D8D74A3C76E6F1E4656AA8",
+  "00CD52DBAA33B063C3A6CD8058A1FB0A46A4754B034FCC644766CA14DA8CA5CA9F"
++ "DE00E88C1AD60CCBA759025299079D7A427EC3CC5B619BFBC828E7769BCD694E86",
+
+  "014BEE21A18B6D8B3C93FAB08D43E739707953244FDBE924FA926D76669E7AC8C8"
++ "9DF62ED8975C2D8397A65A49DCC09F6B0AC62272741924D479354D74FF6075578C",
+  "0133330865C067A0EAF72362A65E2D7BC4E461E8C8995C3B6226A21BD1AA78F0ED"
++ "94FE536A0DCA35534F0CD1510C41525D163FE9D74D134881E35141ED5E8E95B979",
+
+  "013E99020ABF5CEE7525D16B69B229652AB6BDF2AFFCAEF38773B4B7D08725F10C"
++ "DB93482FDCC54EDCEE91ECA4166B2A7C6265EF0CE2BD7051B7CEF945BABD47EE6D",
+  "01FBD0013C674AA79CB39849527916CE301C66EA7CE8B80682786AD60F98F7E78A"
++ "19CA69EFF5C57400E3B3A0AD66CE0978214D13BAF4E9AC60752F7B155E2DE4DCE3"
+	};
+}
diff --git a/Tests/TestEC.cs b/Tests/TestEC.cs
new file mode 100644
index 0000000..6049b07
--- /dev/null
+++ b/Tests/TestEC.cs
@@ -0,0 +1,553 @@
+using System;
+using System.IO;
+using System.Text;
+
+using Crypto;
+
+internal class TestEC {
+
+	internal static void Main(string[] args)
+	{
+		try {
+			TestECInt();
+		} catch (Exception e) {
+			Console.WriteLine(e.ToString());
+			Environment.Exit(1);
+		}
+	}
+
+	internal static void TestECInt()
+	{
+		TestCurve25519();
+		SpeedCurve(EC.Curve25519);
+
+		TestCurve(NIST.P256, KAT_P256);
+		TestCurve(NIST.P384, KAT_P384);
+		TestCurve(NIST.P521, KAT_P521);
+
+		SpeedCurve(NIST.P256);
+		SpeedCurve(NIST.P384);
+		SpeedCurve(NIST.P521);
+	}
+
+	static void TestCurve25519()
+	{
+		Console.Write("Test Curve25519: ");
+
+		TestCurve25519KAT(
+    "a546e36bf0527c9d3b16154b82465edd62144c0ac1fc5a18506a2244ba449ac4",
+    "e6db6867583030db3594c1a424b15f7c726624ec26b3353b10a903a6d0ab1c4c",
+    "c3da55379de9c6908e94ea4df28d084f32eccf03491c71f754b4075577a28552");
+		TestCurve25519KAT(
+    "4b66e9d4d1b4673c5ad22691957d6af5c11b6421e0ea01d42ca4169e7918ba0d",
+    "e5210f12786811d3f4b7959d0538ae2c31dbe7106fc03c3efc4cd549c715a493",
+    "95cbde9476e8907d7aade45cb4b873f88b595a68799fa152e6f8f7647aac7957");
+
+		byte[] u = EC.Curve25519.GetGenerator(false);
+		byte[] k = new byte[u.Length];
+		Array.Copy(u, 0, k, 0, u.Length);
+		Byteswap(k);
+		byte[] nk = new byte[u.Length];
+
+		for (int i = 1; i <= 1000; i ++) {
+			EC.Curve25519.Mul(u, k, nk, false);
+			Array.Copy(k, 0, u, 0, u.Length);
+			Byteswap(u);
+			Array.Copy(nk, 0, k, 0, u.Length);
+			Byteswap(k);
+			if (i == 1) {
+				byte[] z = ToBin(C25519_MC_1);
+				Byteswap(z);
+				if (!Eq(k, z)) {
+					throw new Exception(
+						"Curve25519 MC 1");
+				}
+			} else if (i == 1000) {
+				byte[] z = ToBin(C25519_MC_1000);
+				Byteswap(z);
+				if (!Eq(k, z)) {
+					throw new Exception(
+						"Curve25519 MC 1000");
+				}
+			}
+			if (i % 1000 == 0) {
+				Console.Write(".");
+			}
+		}
+
+		/*
+		Byteswap(k);
+		if (!Eq(k, ToBin(C25519_MC_1000000))) {
+			throw new Exception("Curve25519 MC 1000");
+		}
+		*/
+
+		Console.WriteLine(" done.");
+	}
+
+	static string C25519_MC_1 = "422c8e7a6227d7bca1350b3e2bb7279f7897b87bb6854b783c60e80311ae3079";
+	static string C25519_MC_1000 = "684cf59ba83309552800ef566f2f4d3c1c3887c49360e3875f2eb94d99532c51";
+	/*
+	static string C25519_MC_1000000 = "7c3911e0ab2586fd864497297e575e6f3bc601c0883c30df5f4dd2d24f665424";
+	*/
+
+	static void Byteswap(byte[] t)
+	{
+		for (int i = 0; i < (t.Length >> 1); i ++) {
+			byte x = t[i];
+			t[i] = t[t.Length - 1 - i];
+			t[t.Length - 1 - i] = x;
+		}
+	}
+
+	static void TestCurve25519KAT(string sscalar, string s_in, string s_out)
+	{
+		byte[] tmp = ToBin(sscalar);
+		byte[] scalar = new byte[tmp.Length];
+		for (int i = 0; i < tmp.Length; i ++) {
+			scalar[i] = tmp[tmp.Length - 1 - i];
+		}
+		byte[] A = ToBin(s_in);
+		byte[] B = new byte[A.Length];
+		if (EC.Curve25519.Mul(A, scalar, B, false) != 0xFFFFFFFF) {
+			throw new Exception("Curve25519 multiplication failed");
+		}
+		byte[] C = ToBin(s_out);
+		if (!Eq(B, C)) {
+			throw new Exception("Curve25519 KAT failed");
+		}
+		Console.Write(".");
+	}
+
+	static void TestCurve(ECCurve curve, string[] kat)
+	{
+		Console.Write("Test {0}: ", curve.Name);
+
+		// ====================================================
+
+		/* obsolete -- DEBUG
+		Console.WriteLine();
+		ZInt p = ZInt.DecodeUnsignedBE(((ECCurvePrime)curve).mod);
+		ZInt a = ZInt.DecodeUnsignedBE(((ECCurvePrime)curve).a);
+		ZInt b = ZInt.DecodeUnsignedBE(((ECCurvePrime)curve).b);
+		Console.WriteLine("p  = {0}", p.ToHexString());
+		Console.WriteLine("a  = {0}", a.ToHexString());
+		Console.WriteLine("b  = {0}", b.ToHexString());
+		MutableECPoint F1 = curve.MakeGenerator();
+		byte[] enc = F1.Encode(false);
+		int flen = enc.Length >> 1;
+		for (int i = 0; i < enc.Length; i ++) {
+			if (i == 1 || i == 1 + (enc.Length >> 1)) {
+				Console.Write(" ");
+			}
+			Console.Write("{0:X2}", enc[i]);
+		}
+		Console.WriteLine();
+		byte[] X = new byte[flen];
+		byte[] Y = new byte[flen];
+		Array.Copy(enc, 1, X, 0, flen);
+		Array.Copy(enc, 1 + flen, Y, 0, flen);
+		ZInt x1 = ZInt.DecodeUnsignedBE(X);
+		ZInt y1 = ZInt.DecodeUnsignedBE(Y);
+		Console.WriteLine("X1 = {0}", x1.ToHexString());
+		Console.WriteLine("Y1 = {0}", y1.ToHexString());
+		MutableECPoint F2 = F1.Dup();
+		F2.DoubleCT();
+		MutableECPoint F3 = F2.Dup();
+		MutableECPoint F4 = F2.Dup();
+		enc = F2.Encode(false);
+		for (int i = 0; i < enc.Length; i ++) {
+			if (i == 1 || i == 1 + (enc.Length >> 1)) {
+				Console.Write(" ");
+			}
+			Console.Write("{0:X2}", enc[i]);
+		}
+		Console.WriteLine();
+		Array.Copy(enc, 1, X, 0, flen);
+		Array.Copy(enc, 1 + flen, Y, 0, flen);
+		ZInt x2 = ZInt.DecodeUnsignedBE(X);
+		ZInt y2 = ZInt.DecodeUnsignedBE(Y);
+		Console.WriteLine("X2 = {0}", x2.ToHexString());
+		Console.WriteLine("Y2 = {0}", y2.ToHexString());
+		if ((x1 * x1 * x1 + a * x1 + b - y1 * y1) % p != 0) {
+			throw new Exception("Generator not on curve");
+		}
+		if ((x2 * x2 * x2 + a * x2 + b - y2 * y2) % p != 0) {
+			throw new Exception("Double not on curve");
+		}
+
+		if (F3.AddCT(F1) == 0) {
+			throw new Exception("Addition failed");
+		}
+		MutableECPoint F5 = F3.Dup();
+		enc = F3.Encode(false);
+		for (int i = 0; i < enc.Length; i ++) {
+			if (i == 1 || i == 1 + (enc.Length >> 1)) {
+				Console.Write(" ");
+			}
+			Console.Write("{0:X2}", enc[i]);
+		}
+		Console.WriteLine();
+		Array.Copy(enc, 1, X, 0, flen);
+		Array.Copy(enc, 1 + flen, Y, 0, flen);
+		ZInt x3 = ZInt.DecodeUnsignedBE(X);
+		ZInt y3 = ZInt.DecodeUnsignedBE(Y);
+		Console.WriteLine("X3 = {0}", x3.ToHexString());
+		Console.WriteLine("Y3 = {0}", y3.ToHexString());
+		if ((x3 * x3 * x3 + a * x3 + b - y3 * y3) % p != 0) {
+			throw new Exception("Triple not on curve");
+		}
+		ZInt l3 = ((p + y2 - y1)
+			* ZInt.ModPow(p + x2 - x1, p - 2, p)) % p;
+		ZInt x3p = (l3 * l3 + p + p - x1 - x2) % p;
+		ZInt y3p = (l3 * (p + x1 - x3p) + p - y1) % p;
+		Console.WriteLine("X3p = {0}", x3p.ToHexString());
+		Console.WriteLine("Y3p = {0}", y3p.ToHexString());
+		Console.WriteLine("[X:{0}, Y:{1}]", x3 == x3p, y3 == y3p);
+
+		if (F5.AddCT(F4) == 0) {
+			throw new Exception("Addition failed");
+		}
+		enc = F5.Encode(false);
+		for (int i = 0; i < enc.Length; i ++) {
+			if (i == 1 || i == 1 + (enc.Length >> 1)) {
+				Console.Write(" ");
+			}
+			Console.Write("{0:X2}", enc[i]);
+		}
+		Console.WriteLine();
+		Array.Copy(enc, 1, X, 0, flen);
+		Array.Copy(enc, 1 + flen, Y, 0, flen);
+		ZInt x5 = ZInt.DecodeUnsignedBE(X);
+		ZInt y5 = ZInt.DecodeUnsignedBE(Y);
+		Console.WriteLine("X5 = {0}", x5.ToHexString());
+		Console.WriteLine("Y5 = {0}", y5.ToHexString());
+		if ((x5 * x5 * x5 + a * x5 + b - y5 * y5) % p != 0) {
+			throw new Exception("Quintuple not on curve");
+		}
+		ZInt l5 = ((p + y3 - y2)
+			* ZInt.ModPow(p + x3 - x2, p - 2, p)) % p;
+		ZInt x5p = (l5 * l5 + p + p - x2 - x3) % p;
+		ZInt y5p = (l5 * (p + x2 - x5p) + p - y2) % p;
+		Console.WriteLine("X5p = {0}", x5p.ToHexString());
+		Console.WriteLine("Y5p = {0}", y5p.ToHexString());
+		Console.WriteLine("[X:{0}, Y:{1}]", x5 == x5p, y5 == y5p);
+
+		F1.Set(curve.MakeGenerator());
+		if (F1.MulSpecCT(new byte[] { 0x05 }) == 0) {
+			throw new Exception("Multiplication failed");
+		}
+		enc = F1.Encode(false);
+		for (int i = 0; i < enc.Length; i ++) {
+			if (i == 1 || i == 1 + (enc.Length >> 1)) {
+				Console.Write(" ");
+			}
+			Console.Write("{0:X2}", enc[i]);
+		}
+		Console.WriteLine();
+		Array.Copy(enc, 1, X, 0, flen);
+		Array.Copy(enc, 1 + flen, Y, 0, flen);
+		ZInt x5t = ZInt.DecodeUnsignedBE(X);
+		ZInt y5t = ZInt.DecodeUnsignedBE(Y);
+		Console.WriteLine("X5t = {0}", x5t.ToHexString());
+		Console.WriteLine("Y5t = {0}", y5t.ToHexString());
+		if ((x5t * x5t * x5t + a * x5t + b - y5t * y5t) % p != 0) {
+			throw new Exception("Quintuple not on curve (2)");
+		}
+		Console.WriteLine("[X:{0}, Y:{1}]", x5t == x5p, y5t == y5p);
+
+		F1.Set(F5);
+		F2.SetZero();
+		if (F1.AddCT(F2) == 0) {
+			throw new Exception("Addition failed (+0)");
+		}
+		enc = F1.Encode(false);
+		for (int i = 0; i < enc.Length; i ++) {
+			if (i == 1 || i == 1 + (enc.Length >> 1)) {
+				Console.Write(" ");
+			}
+			Console.Write("{0:X2}", enc[i]);
+		}
+		Console.WriteLine();
+		Array.Copy(enc, 1, X, 0, flen);
+		Array.Copy(enc, 1 + flen, Y, 0, flen);
+		ZInt x5q = ZInt.DecodeUnsignedBE(X);
+		ZInt y5q = ZInt.DecodeUnsignedBE(Y);
+		Console.WriteLine("X5q = {0}", x5q.ToHexString());
+		Console.WriteLine("Y5q = {0}", y5q.ToHexString());
+		Console.WriteLine("[X:{0}, Y:{1}]", x5q == x5p, y5q == y5p);
+
+		F2.SetZero();
+		if (F2.AddCT(F1) == 0) {
+			throw new Exception("Addition failed (0+)");
+		}
+		enc = F2.Encode(false);
+		for (int i = 0; i < enc.Length; i ++) {
+			if (i == 1 || i == 1 + (enc.Length >> 1)) {
+				Console.Write(" ");
+			}
+			Console.Write("{0:X2}", enc[i]);
+		}
+		Console.WriteLine();
+		Array.Copy(enc, 1, X, 0, flen);
+		Array.Copy(enc, 1 + flen, Y, 0, flen);
+		ZInt x5r = ZInt.DecodeUnsignedBE(X);
+		ZInt y5r = ZInt.DecodeUnsignedBE(Y);
+		Console.WriteLine("X5r = {0}", x5r.ToHexString());
+		Console.WriteLine("Y5r = {0}", y5r.ToHexString());
+		Console.WriteLine("[X:{0}, Y:{1}]", x5r == x5p, y5r == y5p);
+
+		EC rG = EC.Make(p.ToBytesUnsignedBE(),
+			a.ToBytesUnsignedBE(), b.ToBytesUnsignedBE());
+		rG.Set(x1.ToBytesUnsignedBE(), y1.ToBytesUnsignedBE());
+		for (int i = 1; i <= 30; i ++) {
+			Console.Write(".");
+			ZInt n = ZInt.MakeRand(i);
+			byte[] nb = n.ToBytesUnsignedBE();
+			F1 = curve.MakeGenerator();
+			if (F1.MulSpecCT(nb) == 0) {
+				throw new Exception("Multiplication error");
+			}
+			enc = F1.Encode(false);
+			ZInt xp, yp;
+			if (enc.Length == 1) {
+				xp = 0;
+				yp = 0;
+			} else {
+				Array.Copy(enc, 1, X, 0, flen);
+				Array.Copy(enc, 1 + flen, Y, 0, flen);
+				xp = ZInt.DecodeUnsignedBE(X);
+				yp = ZInt.DecodeUnsignedBE(Y);
+			}
+			EC rH = rG.Dup();
+			rH.Mul(nb);
+			ZInt xh = ZInt.DecodeUnsignedBE(rH.X);
+			ZInt yh = ZInt.DecodeUnsignedBE(rH.Y);
+			if (xp != xh || yp != yh) {
+				Console.WriteLine();
+				Console.WriteLine("n = {0}", n);
+				Console.WriteLine("xp = {0}", xp.ToHexString());
+				Console.WriteLine("yp = {0}", yp.ToHexString());
+				Console.WriteLine("xh = {0}", xh.ToHexString());
+				Console.WriteLine("yh = {0}", yh.ToHexString());
+				throw new Exception("Bad mult result");
+			}
+		}
+		Console.WriteLine();
+		*/
+
+		// ====================================================
+
+		curve.CheckValid();
+		MutableECPoint G = curve.MakeGenerator();
+		if (G.IsInfinity) {
+			throw new Exception("Generator is infinity");
+		}
+		MutableECPoint P = G.Dup();
+		MutableECPoint Q = G.Dup();
+		MutableECPoint R = G.Dup();
+		MutableECPoint S = G.Dup();
+		MutableECPoint T = G.Dup();
+
+		for (int i = 0; i < 10; i ++) {
+			Console.Write(".");
+			byte[] u, v, w;
+			u = MakeRandPoint(P);
+			do {
+				v = MakeRandPoint(Q);
+			} while (BigInt.Compare(u, v) == 0);
+			// byte[] s = BigInt.Add(u, v);
+			byte[] t;
+			do {
+				w = MakeRandPoint(R);
+				t = BigInt.Add(v, w);
+			} while (BigInt.Compare(u, w) == 0
+				|| BigInt.Compare(v, w) == 0
+				|| BigInt.Compare(u, t) == 0);
+			if (P.Eq(Q) || P.Eq(R) || Q.Eq(R)) {
+				throw new Exception("Equal points");
+			}
+			S.Set(P);
+			Add(S, Q);
+			Add(S, R);
+			T.Set(Q);
+			Add(T, R);
+			Add(T, P);
+			if (!S.Eq(T) || !T.Eq(S)) {
+				throw new Exception("Associativity error");
+			}
+			S.Normalize();
+			if (!S.Eq(T) || !T.Eq(S)) {
+				throw new Exception("Normalization error (1)");
+			}
+			T.Normalize();
+			if (!S.Eq(T) || !T.Eq(S)) {
+				throw new Exception("Normalization error (2)");
+			}
+
+			byte[] enc1 = P.Encode(false);
+			byte[] enc2 = P.Encode(true);
+			byte[] enc3 = new byte[enc1.Length];
+			Array.Copy(enc1, 1, enc3, 1, enc1.Length - 1);
+			enc3[0] = (byte)(enc2[0] | 0x04);
+			Q.Decode(enc1);
+			if (!P.Eq(Q) || !Q.Eq(P)) {
+				throw new Exception("Encode/decode error 1");
+			}
+			Q.Decode(enc2);
+			if (!P.Eq(Q) || !Q.Eq(P)) {
+				throw new Exception("Encode/decode error 2");
+			}
+			Q.Decode(enc3);
+			if (!P.Eq(Q) || !Q.Eq(P)) {
+				throw new Exception("Encode/decode error 3");
+			}
+		}
+
+		Console.Write(" ");
+		for (int i = 0; i < kat.Length; i += 2) {
+			P.Set(G);
+			byte[] n = ToBin(kat[i]);
+			if (P.MulSpecCT(n) == 0) {
+				throw new Exception("Multiplication error");
+			}
+			byte[] er = ToBin(kat[i + 1]);
+			if (!Eq(er, P.Encode(false))) {
+				throw new Exception("KAT failed");
+			}
+			byte[] eg = curve.GetGenerator(false);
+			byte[] ed = new byte[eg.Length];
+			curve.Mul(eg, n, ed, false);
+			if (!Eq(ed, er)) {
+				throw new Exception("KAT failed (API 2)");
+			}
+			Console.Write(".");
+		}
+
+		Console.WriteLine();
+	}
+
+	static void SpeedCurve(ECCurve curve)
+	{
+		byte[][] nn = new byte[100][];
+		for (int i = 0; i < nn.Length; i ++) {
+			nn[i] = BigInt.RandIntNZ(curve.SubgroupOrder);
+		}
+		MutableECPoint G = curve.MakeGenerator();
+		MutableECPoint P = G.Dup();
+		int num = 1;
+		for (;;) {
+			long orig = DateTime.Now.Ticks;
+			for (int i = 0, j = 0; i < num; i ++) {
+				P.MulSpecCT(nn[j]);
+				if (++ j == nn.Length) {
+					j = 0;
+				}
+			}
+			long end = DateTime.Now.Ticks;
+			double tt = (double)(end - orig) / 10000000.0;
+			if (tt < 2.0) {
+				num <<= 1;
+				continue;
+			}
+			double f = (double)num / tt;
+			Console.WriteLine("{0,10}  {1,9:f3} mul/s",
+				curve.Name, f);
+			return;
+		}
+	}
+
+	/*
+	 * Create a random non-infinity point by multiplying the
+	 * curve subgroup generator with a random non-zero integer
+	 * modulo the subgroup order. The multiplier is returned.
+	 */
+	static byte[] MakeRandPoint(MutableECPoint P)
+	{
+		ECCurve curve = P.Curve;
+		P.Set(curve.MakeGenerator());
+		byte[] n = BigInt.RandIntNZ(curve.SubgroupOrder);
+		if (P.MulSpecCT(n) == 0) {
+			throw new Exception("Multiplication failed");
+		}
+		return n;
+	}
+
+	static void Add(MutableECPoint P, MutableECPoint Q)
+	{
+		if (P.Eq(Q)) {
+			P.DoubleCT();
+		} else {
+			if (P.AddCT(Q) == 0) {
+				throw new Exception("Addition failed");
+			}
+		}
+	}
+
+	static bool Eq(byte[] a1, byte[] a2)
+	{
+		if (a1 == a2) {
+			return true;
+		}
+		if (a1 == null || a2 == null) {
+			return false;
+		}
+		int n = a1.Length;
+		if (n != a2.Length) {
+			return false;
+		}
+		for (int i = 0; i < n; i ++) {
+			if (a1[i] != a2[i]) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	static byte[] ToBin(string str)
+	{
+		MemoryStream ms = new MemoryStream();
+		bool z = true;
+		int acc = 0;
+		foreach (char c in str) {
+			int d;
+			if (c >= '0' && c <= '9') {
+				d = c - '0';
+			} else if (c >= 'A' && c <= 'F') {
+				d = c - ('A' - 10);
+			} else if (c >= 'a' && c <= 'f') {
+				d = c - ('a' - 10);
+			} else if (c == ' ' || c == '\t' || c == ':') {
+				continue;
+			} else {
+				throw new ArgumentException(String.Format(
+					"not hex: U+{0:X4}", (int)c));
+			}
+			if (z) {
+				acc = d;
+			} else {
+				ms.WriteByte((byte)((acc << 4) + d));
+			}
+			z = !z;
+		}
+		if (!z) {
+			throw new ArgumentException("final half byte");
+		}
+		return ms.ToArray();
+	}
+
+	static string[] KAT_P256 = {
+		"C9AFA9D845BA75166B5C215767B1D6934E50C3DB36E89B127B8A622B120F6721",
+		"0460FED4BA255A9D31C961EB74C6356D68C049B8923B61FA6CE669622E60F29FB67903FE1008B8BC99A41AE9E95628BC64F2F1B20C2D7E9F5177A3C294D4462299"
+	};
+
+	static string[] KAT_P384 = {
+		"6B9D3DAD2E1B8C1C05B19875B6659F4DE23C3B667BF297BA9AA47740787137D896D5724E4C70A825F872C9EA60D2EDF5",
+		"04EC3A4E415B4E19A4568618029F427FA5DA9A8BC4AE92E02E06AAE5286B300C64DEF8F0EA9055866064A254515480BC138015D9B72D7D57244EA8EF9AC0C621896708A59367F9DFB9F54CA84B3F1C9DB1288B231C3AE0D4FE7344FD2533264720"
+	};
+
+	static string[] KAT_P521 = {
+		"00FAD06DAA62BA3B25D2FB40133DA757205DE67F5BB0018FEE8C86E1B68C7E75CAA896EB32F1F47C70855836A6D16FCC1466F6D8FBEC67DB89EC0C08B0E996B83538",
+		"0401894550D0785932E00EAA23B694F213F8C3121F86DC97A04E5A7167DB4E5BCD371123D46E45DB6B5D5370A7F20FB633155D38FFA16D2BD761DCAC474B9A2F5023A400493101C962CD4D2FDDF782285E64584139C2F91B47F87FF82354D6630F746A28A0DB25741B5B34A828008B22ACC23F924FAAFBD4D33F81EA66956DFEAA2BFDFCF5"
+	};
+}
diff --git a/Tests/TestMath.cs b/Tests/TestMath.cs
new file mode 100644
index 0000000..8d4c65a
--- /dev/null
+++ b/Tests/TestMath.cs
@@ -0,0 +1,174 @@
+using System;
+using System.IO;
+using System.Reflection;
+using System.Text;
+
+using Crypto;
+
+internal class TestMath {
+
+	internal static void Main(string[] args)
+	{
+		try {
+			TestModInt();
+		} catch (Exception e) {
+			Console.WriteLine(e.ToString());
+			Environment.Exit(1);
+		}
+	}
+
+	static ZInt RandPrime(int k)
+	{
+		if (k < 2) {
+			throw new ArgumentException();
+		}
+		ZInt min = ZInt.One << (k - 1);
+		ZInt max = ZInt.One << k;
+		for (;;) {
+			ZInt p = ZInt.MakeRand(min, max) | 1;
+			if (p.IsPrime) {
+				return p;
+			}
+		}
+	}
+
+	internal static void TestModInt()
+	{
+		Console.Write("Test ModInt: ");
+		for (int k = 2; k <= 128; k ++) {
+			for (int i = 0; i < 10; i ++) {
+				int kwlen = (k + 30) / 31;
+				int kwb = 31 * kwlen;
+
+				ZInt p;
+				if (k >= 9) {
+					p = ZInt.DecodeUnsignedBE(
+						BigInt.RandPrime(k));
+					if (p.BitLength != k) {
+						throw new Exception(
+							"wrong prime size");
+					}
+					if (!p.IsPrime) {
+						throw new Exception(
+							"not prime");
+					}
+				} else {
+					p = RandPrime(k);
+				}
+
+				ZInt a = ZInt.MakeRand(p);
+				ZInt b = ZInt.MakeRand(p);
+				ZInt v = ZInt.MakeRand(k + 60);
+				if (b == ZInt.Zero) {
+					b = ZInt.One;
+				}
+				byte[] ea = a.ToBytesBE();
+				byte[] eb = b.ToBytesBE();
+				byte[] ev = v.ToBytesBE();
+				ModInt mz = new ModInt(p.ToBytesBE());
+				ModInt ma = mz.Dup();
+				ModInt mb = mz.Dup();
+
+				ma.Decode(ea);
+				CheckEq(ma, a);
+
+				ma.Decode(ea);
+				mb.Decode(eb);
+				ma.Add(mb);
+				CheckEq(ma, (a + b).Mod(p));
+
+				ma.Decode(ea);
+				mb.Decode(eb);
+				ma.Sub(mb);
+				CheckEq(ma, (a - b).Mod(p));
+
+				ma.Decode(ea);
+				ma.Negate();
+				CheckEq(ma, (-a).Mod(p));
+
+				ma.Decode(ea);
+				mb.Decode(eb);
+				ma.MontyMul(mb);
+				CheckEq((ZInt.DecodeUnsignedBE(ma.Encode())
+					<< kwb).Mod(p), (a * b).Mod(p));
+
+				ma.Decode(ea);
+				ma.ToMonty();
+				CheckEq(ma, (a << kwb).Mod(p));
+				ma.FromMonty();
+				CheckEq(ma, a);
+
+				ma.Decode(ea);
+				mb.Decode(eb);
+				ma.ToMonty();
+				mb.ToMonty();
+				ma.MontyMul(mb);
+				ma.FromMonty();
+				CheckEq(ma, (a * b).Mod(p));
+
+				mb.Decode(eb);
+				mb.Invert();
+				ZInt r = ZInt.DecodeUnsignedBE(mb.Encode());
+				CheckEq(ZInt.One, (r * b).Mod(p));
+
+				ma.Decode(ea);
+				ma.Pow(ev);
+				CheckEq(ma, ZInt.ModPow(a, v, p));
+
+				ma.DecodeReduce(ev);
+				CheckEq(ma, v.Mod(p));
+
+				mb.Decode(eb);
+				ma.Set(mb);
+				CheckEq(ma, b);
+
+				ModInt mv = new ModInt(
+					((p << 61) + 1).ToBytesBE());
+				mv.Decode(ev);
+				ma.Set(mv);
+				CheckEq(ma, v.Mod(p));
+
+				if (k >= 9) {
+					ma.Decode(ea);
+					mb.Set(ma);
+					mb.ToMonty();
+					mb.MontyMul(ma);
+					if ((int)mb.SqrtBlum() != -1) {
+						throw new CryptoException(
+							"square root failed");
+					}
+					if (!mb.Eq(ma)) {
+						mb.Negate();
+					}
+					CheckEq(mb, a);
+
+					mb.Decode(eb);
+					mb.ToMonty();
+					mb.MontySquare();
+					mb.FromMonty();
+					mb.Negate();
+					if (mb.SqrtBlum() != 0) {
+						throw new CryptoException(
+							"square root should"
+							+ " have failed");
+					}
+				}
+			}
+			Console.Write(".");
+		}
+		Console.WriteLine(" done.");
+	}
+
+	static void CheckEq(ModInt m, ZInt z)
+	{
+		CheckEq(ZInt.DecodeUnsignedBE(m.Encode()), z);
+	}
+
+	static void CheckEq(ZInt x, ZInt z)
+	{
+		if (x != z) {
+			throw new Exception(String.Format(
+				"mismatch: x={0} z={1}", x, z));
+		}
+	}
+}
diff --git a/Twrch/JSON.cs b/Twrch/JSON.cs
new file mode 100644
index 0000000..9392847
--- /dev/null
+++ b/Twrch/JSON.cs
@@ -0,0 +1,694 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+/*
+ * A simple JSON parser.
+ *
+ * A JSON value is returned as:
+ *
+ *   - null, if the value is a JSON null;
+ *
+ *   - a string, if the value is a JSON string, a JSON number or a
+ *     JSON boolean;
+ *
+ *   - an IDictionary<string, object>, if the value is a JSON object;
+ *
+ *   - an array (object[]), if the value is a JSON array.
+ *
+ * This parser is lenient with numbers, in that it will gleefully
+ * accumulate digits, dots, minus sign, plus sign, lowercase 'e'
+ * and uppercase 'E' characters in any order.
+ */
+
+public static class JSON {
+
+	/*
+	 * Parse a source stream as a JSON object.
+	 */
+	public static object Parse(Stream src)
+	{
+		return Parse(new StreamReader(src));
+	}
+
+	/*
+	 * Parse a source stream as a JSON object.
+	 */
+	public static object Parse(TextReader tr)
+	{
+		int cp = NextNonWS(tr, ' ');
+		object val;
+		cp = ReadValue(tr, cp, out val);
+		while (cp >= 0) {
+			if (!IsWS(cp)) {
+				throw new Exception(
+					"Trailing garbage after JSON value");
+			}
+			cp = tr.Read();
+		}
+		return val;
+	}
+
+	/*
+	 * Encode a JSON object onto a stream.
+	 */
+	public static void Encode(object obj, Stream dst)
+	{
+		TextWriter tw = new StreamWriter(dst);
+		Encode(obj, tw);
+		tw.Flush();
+	}
+
+	/*
+	 * Encode a JSON object onto a stream.
+	 */
+	public static void Encode(object obj, TextWriter tw)
+	{
+		EncodeValue(0, obj, tw);
+		tw.WriteLine();
+	}
+
+	/*
+	 * Get a value by path. If the value is present, then 'val'
+	 * is set to that value (which may be null) and true is returned;
+	 * otherwise, 'val' is set to null and false is written.
+	 *
+	 * An exception is still thrown if one of the upper path elements
+	 * does not have the expected type.
+	 */
+	public static bool TryGet(object obj, string path, out object val)
+	{
+		int n = path.Length;
+		int p = 0;
+		while (p < n) {
+			int q = path.IndexOf('/', p);
+			if (q < 0) {
+				q = n;
+			}
+			IDictionary<string, object> d =
+				obj as IDictionary<string, object>;
+			if (d == null) {
+				throw new Exception(string.Format(
+					"Path '{0}': not an object",
+					path.Substring(0, p)));
+			}
+			string name = path.Substring(p, q - p);
+			if (!d.ContainsKey(name)) {
+				val = null;
+				return false;
+			}
+			obj = d[name];
+			p = q + 1;
+		}
+		val = obj;
+		return true;
+	}
+
+	/*
+	 * Get a value by path.
+	 */
+	public static object Get(object obj, string path)
+	{
+		object val;
+		if (!TryGet(obj, path, out val)) {
+			throw new Exception("No such value: " + path);
+		}
+		return val;
+	}
+
+	/*
+	 * Try to get a value by path; value (if present) should be a
+	 * string.
+	 */
+	public static bool TryGetString(object obj, string path, out string val)
+	{
+		object gv;
+		if (!TryGet(obj, path, out gv)) {
+			val = null;
+			return false;
+		}
+		if (!(gv is string)) {
+			throw new Exception("Value at " + path
+				+ " is not a string");
+		}
+		val = gv as string;
+		return true;
+	}
+
+	/*
+	 * Get a value by path; value should be a string.
+	 */
+	public static string GetString(object obj, string path)
+	{
+		string str;
+		if (!TryGetString(obj, path, out str)) {
+			throw new Exception("No such value: " + path);
+		}
+		return str;
+	}
+
+	/*
+	 * Try to get a value by path; value should be an array.
+	 */
+	public static bool TryGetArray(object obj, string path,
+		out object[] val)
+	{
+		object gv;
+		if (!TryGet(obj, path, out gv)) {
+			val = null;
+			return false;
+		}
+		val = gv as object[];
+		if (val == null) {
+			throw new Exception("Value at " + path
+				+ " is not an array");
+		}
+		return true;
+	}
+
+	/*
+	 * Get a value by path; value should be an array.
+	 */
+	public static object[] GetArray(object obj, string path)
+	{
+		object[] a;
+		if (!TryGetArray(obj, path, out a)) {
+			throw new Exception("No such value: " + path);
+		}
+		return a;
+	}
+
+	/*
+	 * Try to get a value by path; if present, value should be an
+	 * array, whose elements are all strings. A new, properly typed
+	 * array is returned, containing the strings.
+	 */
+	public static bool TryGetStringArray(object obj, string path,
+		out string[] a)
+	{
+		object[] g;
+		if (!TryGetArray(obj, path, out g)) {
+			a = null;
+			return false;
+		}
+		string[] r = new string[g.Length];
+		for (int i = 0; i < g.Length; i ++) {
+			string s = g[i] as string;
+			if (s == null) {
+				throw new Exception(string.Format("Element {0}"
+					+ " in array {1} is not a string",
+					i, path));
+			}
+			r[i] = s;
+		}
+		a = r;
+		return true;
+	}
+
+	/*
+	 * Get a value by path; value should be an array, whose
+	 * elements are all strings. A new, properly typed array is
+	 * returned, containing the strings.
+	 */
+	public static string[] GetStringArray(object obj, string path)
+	{
+		string[] a;
+		if (!TryGetStringArray(obj, path, out a)) {
+			throw new Exception("No such value: " + path);
+		}
+		return a;
+	}
+
+	/*
+	 * Try to get a value by path; value should a boolean.
+	 */
+	public static bool TryGetBool(object obj, string path, out bool val)
+	{
+		object gv;
+		if (!TryGet(obj, path, out gv)) {
+			val = false;
+			return false;
+		}
+		if (gv is bool) {
+			val = (bool)gv;
+			return true;
+		} else if (gv is string) {
+			switch (gv as string) {
+			case "true":   val = true; return true;
+			case "false":  val = false; return true;
+			}
+		}
+		throw new Exception("Value at " + path + " is not a boolean");
+	}
+
+	/*
+	 * Get a value by path; value should a boolean.
+	 */
+	public static bool GetBool(object obj, string path)
+	{
+		bool v;
+		if (!TryGetBool(obj, path, out v)) {
+			throw new Exception("No such value: " + path);
+		}
+		return v;
+	}
+
+	/*
+	 * Try to get a value by path; value should an integer.
+	 */
+	public static bool TryGetInt32(object obj, string path, out int val)
+	{
+		object gv;
+		if (!TryGet(obj, path, out gv)) {
+			val = 0;
+			return false;
+		}
+		if (gv is int) {
+			val = (int)gv;
+			return true;
+		} else if (gv is uint) {
+			uint x = (uint)gv;
+			if (x <= (uint)Int32.MaxValue) {
+				val = (int)x;
+				return true;
+			}
+		} else if (gv is long) {
+			long x = (long)gv;
+			if (x >= (long)Int32.MinValue
+				&& x <= (long)Int32.MaxValue)
+			{
+				val = (int)x;
+				return true;
+			}
+		} else if (gv is ulong) {
+			ulong x = (ulong)gv;
+			if (x <= (ulong)Int32.MaxValue) {
+				val = (int)x;
+				return true;
+			}
+		} else if (gv is string) {
+			int x;
+			if (Int32.TryParse((string)gv, out x)) {
+				val = x;
+				return true;
+			}
+		}
+		throw new Exception("Value at " + path + " is not a boolean");
+	}
+
+	/*
+	 * Get a value by path; value should an integer.
+	 */
+	public static int GetInt32(object obj, string path)
+	{
+		int v;
+		if (!TryGetInt32(obj, path, out v)) {
+			throw new Exception("No such value: " + path);
+		}
+		return v;
+	}
+
+	/*
+	 * Try to get a value by path; value should be an object map.
+	 */
+	public static bool TryGetObjectMap(object obj, string path,
+		out IDictionary<string, object> val)
+	{
+		object gv;
+		if (!TryGet(obj, path, out gv)) {
+			val = null;
+			return false;
+		}
+		val = gv as IDictionary<string, object>;
+		if (val == null) {
+			throw new Exception("Value at " + path
+				+ " is not an object map");
+		}
+		return true;
+	}
+
+	/*
+	 * Get a value by path; value should be an object map.
+	 */
+	public static IDictionary<string, object> GetObjectMap(
+		object obj, string path)
+	{
+		IDictionary<string, object> v;
+		if (!TryGetObjectMap(obj, path, out v)) {
+			throw new Exception("No such value: " + path);
+		}
+		return v;
+	}
+
+	static void EncodeValue(int indent, object obj, TextWriter tw)
+	{
+		if (obj == null) {
+			tw.Write("null");
+			return;
+		}
+		if (obj is bool) {
+			tw.Write((bool)obj ? "true" : "false");
+			return;
+		}
+		if (obj is string) {
+			EncodeString((string)obj, tw);
+			return;
+		}
+		if (obj is int || obj is uint || obj is long || obj is ulong) {
+			tw.Write(obj.ToString());
+			return;
+		}
+		if (obj is Array) {
+			tw.Write("[");
+			Array a = (Array)obj;
+			for (int i = 0; i < a.Length; i ++) {
+				if (i != 0) {
+					tw.Write(",");
+				}
+				tw.WriteLine();
+				Indent(indent + 1, tw);
+				EncodeValue(indent + 1, a.GetValue(i), tw);
+			}
+			tw.WriteLine();
+			Indent(indent, tw);
+			tw.Write("]");
+			return;
+		}
+		if (obj is IDictionary<string, object>) {
+			tw.Write("{");
+			IDictionary<string, object> d =
+				(IDictionary<string, object>)obj;
+			bool first = true;
+			foreach (string name in d.Keys) {
+				if (first) {
+					first = false;
+				} else {
+					tw.Write(",");
+				}
+				tw.WriteLine();
+				Indent(indent + 1, tw);
+				EncodeString(name, tw);
+				tw.Write(" : ");
+				EncodeValue(indent + 1, d[name], tw);
+			}
+			tw.WriteLine();
+			Indent(indent, tw);
+			tw.Write("}");
+			return;
+		}
+		throw new Exception("Unknown value type: "
+			+ obj.GetType().FullName);
+	}
+
+	static void Indent(int indent, TextWriter tw)
+	{
+		while (indent -- > 0) {
+			tw.Write("  ");
+		}
+	}
+
+	static void EncodeString(string str, TextWriter tw)
+	{
+		tw.Write('\"');
+		foreach (char c in str) {
+			if (c >= 32 && c <= 126) {
+				if (c == '\\' || c == '"') {
+					tw.Write('\\');
+				}
+				tw.Write(c);
+			} else {
+				switch (c) {
+				case '\b':
+					tw.Write("\\b");
+					break;
+				case '\f':
+					tw.Write("\\f");
+					break;
+				case '\n':
+					tw.Write("\\n");
+					break;
+				case '\r':
+					tw.Write("\\r");
+					break;
+				case '\t':
+					tw.Write("\\t");
+					break;
+				default:
+					tw.Write("\\u{0:X4}", (int)c);
+					break;
+				}
+			}
+		}
+		tw.Write('\"');
+	}
+
+	/*
+	 * Read a value, that starts with the provided character. The
+	 * value is written in 'val'. Returned value is the next
+	 * character in the stream, or a synthetic space if the next
+	 * character was not read.
+	 */
+	static int ReadValue(TextReader tr, int cp, out object val)
+	{
+		switch (cp) {
+		case '"':
+			val = ReadString(tr);
+			return ' ';
+		case '{':
+			val = ReadObject(tr);
+			return ' ';
+		case '[':
+			val = ReadArray(tr);
+			return ' ';
+		case 't':
+			CheckKeyword(tr, "true");
+			val = "true";
+			return ' ';
+		case 'f':
+			CheckKeyword(tr, "false");
+			val = "false";
+			return ' ';
+		case 'n':
+			CheckKeyword(tr, "null");
+			val = null;
+			return ' ';
+		case '-':
+		case '0': case '1': case '2': case '3': case '4':
+		case '5': case '6': case '7': case '8': case '9':
+			StringBuilder sb = new StringBuilder();
+			sb.Append((char)cp);
+			cp = ReadNumber(tr, sb);
+			val = sb.ToString();
+			return cp;
+		default:
+			throw Unexpected(cp);
+		}
+	}
+
+	static string ReadString(TextReader tr)
+	{
+		StringBuilder sb = new StringBuilder();
+		bool lcwb = false;
+		for (;;) {
+			int cp = Next(tr);
+			if (lcwb) {
+				lcwb = false;
+				switch (cp) {
+				case '"': case '\\': case '/':
+					sb.Append((char)cp);
+					break;
+				case 'b':
+					sb.Append('\b');
+					break;
+				case 'f':
+					sb.Append('\f');
+					break;
+				case 'n':
+					sb.Append('\n');
+					break;
+				case 'r':
+					sb.Append('\r');
+					break;
+				case 't':
+					sb.Append('\t');
+					break;
+				case 'u':
+					sb.Append(ReadUnicodeEscape(tr));
+					break;
+				default:
+					throw Unexpected(cp);
+				}
+			} else {
+				if (cp == '\\') {
+					lcwb = true;
+				} else if (cp == '"') {
+					return sb.ToString();
+				} else if (cp <= 0x1F) {
+					throw Unexpected(cp);
+				} else {
+					sb.Append((char)cp);
+				}
+			}
+		}
+	}
+
+	static char ReadUnicodeEscape(TextReader tr)
+	{
+		int acc = 0;
+		for (int i = 0; i < 4; i ++) {
+			int cp = Next(tr);
+			if (cp >= '0' && cp <= '9') {
+				cp -= '0';
+			} else if (cp >= 'A' && cp <= 'F') {
+				cp -= 'A' - 10;
+			} else if (cp >= 'a' && cp <= 'f') {
+				cp -= 'a' - 10;
+			} else {
+				throw Unexpected(cp);
+			}
+			acc = (acc << 4) + cp;
+		}
+		return (char)acc;
+	}
+
+	static IDictionary<string, object> ReadObject(TextReader tr)
+	{
+		IDictionary<string, object> r =
+			new SortedDictionary<string, object>(
+				StringComparer.Ordinal);
+		int cp = NextNonWS(tr, ' ');
+		if (cp == '}') {
+			return r;
+		}
+		for (;;) {
+			if (cp != '"') {
+				throw Unexpected(cp);
+			}
+			string name = ReadString(tr);
+			cp = NextNonWS(tr, ' ');
+			if (cp != ':') {
+				throw Unexpected(cp);
+			}
+			if (r.ContainsKey(name)) {
+				throw new Exception(string.Format(
+					"duplicate key '{0}' in object",
+					name));
+			}
+			object val;
+			cp = NextNonWS(tr, ' ');
+			cp = ReadValue(tr, cp, out val);
+			r[name] = val;
+			cp = NextNonWS(tr, cp);
+			if (cp == '}') {
+				return r;
+			}
+			if (cp != ',') {
+				throw Unexpected(cp);
+			}
+			cp = NextNonWS(tr, ' ');
+		}
+	}
+
+	static object[] ReadArray(TextReader tr)
+	{
+		List<object> r = new List<object>();
+		int cp = NextNonWS(tr, ' ');
+		if (cp == ']') {
+			return r.ToArray();
+		}
+		for (;;) {
+			object val;
+			cp = ReadValue(tr, cp, out val);
+			r.Add(val);
+			cp = NextNonWS(tr, cp);
+			if (cp == ']') {
+				return r.ToArray();
+			}
+			if (cp != ',') {
+				throw Unexpected(cp);
+			}
+			cp = NextNonWS(tr, ' ');
+		}
+	}
+
+	static int ReadNumber(TextReader tr, StringBuilder sb)
+	{
+		int cp;
+		for (;;) {
+			cp = tr.Read();
+			switch (cp) {
+			case '0': case '1': case '2': case '3': case '4':
+			case '5': case '6': case '7': case '8': case '9':
+			case '.': case '-': case '+': case 'e': case 'E':
+				sb.Append((char)cp);
+				break;
+			default:
+				return cp;
+			}
+		}
+	}
+
+	static void CheckKeyword(TextReader tr, string str)
+	{
+		int n = str.Length;
+		for (int i = 1; i < n; i ++) {
+			int cp = Next(tr);
+			if (cp != (int)str[i]) {
+				throw Unexpected(cp);
+			}
+		}
+	}
+
+	static bool IsWS(int cp)
+	{
+		return cp == 9 || cp == 10 || cp == 13 || cp == 32;
+	}
+
+	static int Next(TextReader tr)
+	{
+		int cp = tr.Read();
+		if (cp < 0) {
+			throw new EndOfStreamException();
+		}
+		return cp;
+	}
+
+	static int NextNonWS(TextReader tr, int cp)
+	{
+		while (IsWS(cp)) {
+			cp = Next(tr);
+		}
+		return cp;
+	}
+
+	static Exception Unexpected(int cp)
+	{
+		return new Exception(string.Format(
+			"Unexpected character U+{0:X4}", cp));
+	}
+}
diff --git a/Twrch/MergeStream.cs b/Twrch/MergeStream.cs
new file mode 100644
index 0000000..4a5ac29
--- /dev/null
+++ b/Twrch/MergeStream.cs
@@ -0,0 +1,184 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.IO;
+
+/*
+ * This class merges two underlying streams (one for reading, the other
+ * for writing) into a single Stream object. It can also optionally dump
+ * all read and written bytes, in hexadecimal, on an provided text
+ * stream (for debugging purposes).
+ */
+
+internal class MergeStream : Stream {
+
+	Stream subIn, subOut;
+
+	internal TextWriter Debug {
+		get; set;
+	}
+
+	internal MergeStream(Stream subIn, Stream subOut)
+	{
+		this.subIn = subIn;
+		this.subOut = subOut;
+	}
+
+	public override int ReadByte()
+	{
+		int x = subIn.ReadByte();
+		if (Debug != null) {
+			if (x >= 0) {
+				Debug.WriteLine("recv:");
+				Debug.WriteLine("   {0:x2}", x);
+			} else {
+				Debug.WriteLine("recv: EOF");
+			}
+		}
+		return x;
+	}
+
+	public override int Read(byte[] buf, int off, int len)
+	{
+		int rlen = subIn.Read(buf, off, len);
+		if (Debug != null) {
+			if (rlen <= 0) {
+				Debug.WriteLine("recv: EOF");
+			} else {
+				Debug.Write("recv:");
+				for (int i = 0; i < rlen; i ++) {
+					if ((i & 15) == 0) {
+						Debug.WriteLine();
+						Debug.Write("   ");
+					} else if ((i & 7) == 0) {
+						Debug.Write("  ");
+					} else {
+						Debug.Write(" ");
+					}
+					Debug.Write("{0:x2}", buf[i]);
+				}
+				Debug.WriteLine();
+			}
+		}
+		return rlen;
+	}
+
+	public override void WriteByte(byte x)
+	{
+		if (Debug != null) {
+			Debug.WriteLine("send:");
+			Debug.WriteLine("   {0:x2}", x);
+		}
+		subOut.WriteByte(x);
+	}
+
+	public override void Write(byte[] buf, int off, int len)
+	{
+		if (Debug != null) {
+			Debug.Write("send:");
+			for (int i = 0; i < len; i ++) {
+				if ((i & 15) == 0) {
+					Debug.WriteLine();
+					Debug.Write("   ");
+				} else if ((i & 7) == 0) {
+					Debug.Write("  ");
+				} else {
+					Debug.Write(" ");
+				}
+				Debug.Write("{0:x2}", buf[i]);
+			}
+			Debug.WriteLine();
+		}
+		subOut.Write(buf, off, len);
+	}
+
+	public override void Flush()
+	{
+		subOut.Flush();
+	}
+
+	public override void Close()
+	{
+		Exception ex1 = null, ex2 = null;
+		try {
+			subIn.Close();
+		} catch (Exception ex) {
+			ex1 = ex;
+		}
+		try {
+			subOut.Close();
+		} catch (Exception ex) {
+			ex2 = ex;
+		}
+		if (ex2 != null) {
+			throw ex2;
+		} else if (ex1 != null) {
+			throw ex1;
+		}
+	}
+
+	public override long Seek(long off, SeekOrigin origin)
+	{
+		throw new NotSupportedException();
+	}
+
+	public override void SetLength(long len)
+	{
+		throw new NotSupportedException();
+	}
+
+	public override bool CanRead {
+		get {
+			return subIn.CanRead;
+		}
+	}
+
+	public override bool CanWrite {
+		get {
+			return subOut.CanWrite;
+		}
+	}
+
+	public override bool CanSeek {
+		get {
+			return false;
+		}
+	}
+
+	public override long Length {
+		get {
+			throw new NotSupportedException();
+		}
+	}
+
+	public override long Position {
+		get {
+			throw new NotSupportedException();
+		}
+		set {
+			throw new NotSupportedException();
+		}
+	}
+}
diff --git a/Twrch/Twrch.cs b/Twrch/Twrch.cs
new file mode 100644
index 0000000..6692d53
--- /dev/null
+++ b/Twrch/Twrch.cs
@@ -0,0 +1,1336 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.Text;
+
+using Asn1;
+using Crypto;
+using SSLTLS;
+using XKeys;
+
+/*
+ * This is the main Twrch class implementation: it provides the entry
+ * point for the command-line application.
+ */
+
+public class Twrch {
+
+	public static void Main(string[] args)
+	{
+		try {
+			new Twrch().Run(args);
+		} catch (Exception e) {
+			Console.WriteLine(e.ToString());
+			Environment.Exit(1);
+		}
+	}
+
+	bool trace;
+	object conf;
+	string commandFile;
+	string commandArgs;
+	bool commandVerbose;
+	string chainRSAFile;
+	byte[][] chainRSA;
+	string skeyRSAFile;
+	IPrivateKey skeyRSA;
+	string chainECFile;
+	byte[][] chainEC;
+	string skeyECFile;
+	IPrivateKey skeyEC;
+	int[] versions;
+	int versionMin;
+	int versionMax;
+	int[] cipherSuites;
+	int[] hashAndSigns;
+	int[] curves;
+	bool noCloseNotify;
+	object[] tests;
+	IDictionary<string, object> testsByName;
+
+	int totalTests;
+	int totalSuccess;
+	int totalFailures;
+
+	void Run(string[] args)
+	{
+		List<string> r = new List<string>();
+		string confName = null;
+		int doEnum = 0;
+		foreach (string a in args) {
+			string b = a.ToLowerInvariant();
+			switch (b) {
+			case "-trace":
+				trace = true;
+				break;
+			case "-enum":
+				doEnum = 1;
+				break;
+			case "-noenum":
+				doEnum = -1;
+				break;
+			case "-cv":
+				commandVerbose = true;
+				break;
+			default:
+				if (confName == null) {
+					confName = a;
+				} else {
+					r.Add(a);
+				}
+				break;
+			}
+		}
+		if (confName == null) {
+			Usage();
+		}
+		string[] testNames = r.ToArray();
+		conf = ReadConfig(confName);
+		if (doEnum == 0) {
+			doEnum = (testNames.Length == 0) ? 1 : -1;
+		}
+		commandFile = JSON.GetString(conf, "commandFile");
+		commandArgs = JSON.GetString(conf, "commandArgs");
+		chainRSAFile = JSON.GetString(conf, "chainRSA");
+		chainECFile = JSON.GetString(conf, "chainEC");
+		skeyRSAFile = JSON.GetString(conf, "skeyRSA");
+		skeyECFile = JSON.GetString(conf, "skeyEC");
+		chainRSA = DecodeChain(chainRSAFile);
+		skeyRSA = DecodePrivateKey(skeyRSAFile);
+		chainEC = DecodeChain(chainECFile);
+		skeyEC = DecodePrivateKey(skeyECFile);
+		versions = GetVersions();
+		if (versions.Length == 0) {
+			throw new Exception("Bad config: no versions");
+		}
+		versionMin = Int32.MaxValue;
+		versionMax = -1;
+		foreach (int v in versions) {
+			versionMin = Math.Min(v, versionMin);
+			versionMax = Math.Max(v, versionMax);
+		}
+		cipherSuites = GetCipherSuites();
+		if (cipherSuites.Length == 0) {
+			throw new Exception("Bad config: no cipher suites");
+		}
+		hashAndSigns = GetHashAndSigns();
+		if (hashAndSigns.Length == 0) {
+			throw new Exception("Bad config: no hash-and-signs");
+		}
+		curves = GetCurves();
+		noCloseNotify = JSON.GetBool(conf, "noCloseNotify");
+		tests = JSON.GetArray(conf, "tests");
+		testsByName = new SortedDictionary<string, object>(
+			StringComparer.Ordinal);
+		foreach (object obj in tests) {
+			string name = JSON.GetString(obj, "name");
+			testsByName[name] = obj;
+		}
+
+		totalTests = 0;
+		totalSuccess = 0;
+		totalFailures = 0;
+		if (doEnum > 0) {
+			totalTests += ComputeTotalEnum();
+		}
+		if (testNames.Length == 0) {
+			foreach (object obj in tests) {
+				totalTests += GetNumTests(obj);
+			}
+		} else {
+			foreach (string name in testNames) {
+				bool client;
+				int version, suite, curve, hs;
+				if (StringToTEnum(name, out client, out version,
+					out suite, out curve, out hs))
+				{
+					totalTests ++;
+					continue;
+				}
+				if (name.EndsWith("_client")
+					|| name.EndsWith("_server"))
+				{
+					totalTests ++;
+				} else {
+					totalTests += GetNumTests(
+						testsByName[name]);
+				}
+			}
+		}
+
+		if (doEnum > 0) {
+			RunEnum();
+		}
+		if (testNames.Length == 0) {
+			foreach (object obj in tests) {
+				RunTest(obj);
+			}
+		} else {
+			foreach (string name in testNames) {
+				bool client;
+				int version, suite, curve, hs;
+				if (StringToTEnum(name, out client, out version,
+					out suite, out curve, out hs))
+				{
+					RunEnum(client, version,
+						suite, curve, hs);
+					continue;
+				}
+				if (name.EndsWith("_client")) {
+					client = true;
+				} else if (name.EndsWith("_server")) {
+					client = false;
+				} else {
+					RunTest(testsByName[name]);
+					continue;
+				}
+				string s = name.Substring(0, name.Length - 7);
+				RunTest(client, testsByName[s]);
+			}
+		}
+
+		Console.WriteLine();
+		Console.WriteLine("\rtotal = {0}, failed = {1}",
+			totalTests, totalFailures);
+	}
+
+	static void Usage()
+	{
+		Console.WriteLine(
+"usage: Twrch.exe [ options ] config [ test... ]");
+		Console.WriteLine(
+"options:");
+		Console.WriteLine(
+"   -trace    enable trace mode (hex dump of all exchanged bytes)");
+		Console.WriteLine(
+"   -cv       pass the '-v' argument to the test command");
+		Console.WriteLine(
+"   -enum     perform all version/suite/curve/hash&sign combination tests");
+		Console.WriteLine(
+"   -noenum   do NOT perform the version/suite/curve/hash&sign tests");
+		Environment.Exit(1);
+	}
+
+	static object ReadConfig(string fname)
+	{
+		using (TextReader r = File.OpenText(fname)) {
+			return JSON.Parse(r);
+		}
+	}
+
+	int[] GetVersions()
+	{
+		string[] r = JSON.GetStringArray(conf, "versions");
+		int[] vv = new int[r.Length];
+		for (int i = 0; i < r.Length; i ++) {
+			vv[i] = GetVersionByName(r[i]);
+		}
+		return vv;
+	}
+
+	internal static int GetVersionByName(string s)
+	{
+		s = s.Replace(" ", "").Replace(".", "").ToUpperInvariant();
+		switch (s) {
+		case "TLS10": return SSL.TLS10;
+		case "TLS11": return SSL.TLS11;
+		case "TLS12": return SSL.TLS12;
+		default:
+			throw new Exception(string.Format(
+				"Unknown version: '{0}'", s));
+		}
+	}
+
+	int[] GetCipherSuites()
+	{
+		return GetSuitesByName(
+			JSON.GetStringArray(conf, "cipherSuites"));
+	}
+
+	internal static int[] GetSuitesByName(string[] ss)
+	{
+		int[] r = new int[ss.Length];
+		for (int i = 0; i < ss.Length; i ++) {
+			r[i] = GetSuiteByName(ss[i]);
+		}
+		return r;
+	}
+
+	internal static int GetSuiteByName(string s)
+	{
+		switch (s) {
+		case "NULL_WITH_NULL_NULL":
+			return SSL.NULL_WITH_NULL_NULL;
+		case "RSA_WITH_NULL_MD5":
+			return SSL.RSA_WITH_NULL_MD5;
+		case "RSA_WITH_NULL_SHA":
+			return SSL.RSA_WITH_NULL_SHA;
+		case "RSA_WITH_NULL_SHA256":
+			return SSL.RSA_WITH_NULL_SHA256;
+		case "RSA_WITH_RC4_128_MD5":
+			return SSL.RSA_WITH_RC4_128_MD5;
+		case "RSA_WITH_RC4_128_SHA":
+			return SSL.RSA_WITH_RC4_128_SHA;
+		case "RSA_WITH_3DES_EDE_CBC_SHA":
+			return SSL.RSA_WITH_3DES_EDE_CBC_SHA;
+		case "RSA_WITH_AES_128_CBC_SHA":
+			return SSL.RSA_WITH_AES_128_CBC_SHA;
+		case "RSA_WITH_AES_256_CBC_SHA":
+			return SSL.RSA_WITH_AES_256_CBC_SHA;
+		case "RSA_WITH_AES_128_CBC_SHA256":
+			return SSL.RSA_WITH_AES_128_CBC_SHA256;
+		case "RSA_WITH_AES_256_CBC_SHA256":
+			return SSL.RSA_WITH_AES_256_CBC_SHA256;
+		case "DH_DSS_WITH_3DES_EDE_CBC_SHA":
+			return SSL.DH_DSS_WITH_3DES_EDE_CBC_SHA;
+		case "DH_RSA_WITH_3DES_EDE_CBC_SHA":
+			return SSL.DH_RSA_WITH_3DES_EDE_CBC_SHA;
+		case "DHE_DSS_WITH_3DES_EDE_CBC_SHA":
+			return SSL.DHE_DSS_WITH_3DES_EDE_CBC_SHA;
+		case "DHE_RSA_WITH_3DES_EDE_CBC_SHA":
+			return SSL.DHE_RSA_WITH_3DES_EDE_CBC_SHA;
+		case "DH_DSS_WITH_AES_128_CBC_SHA":
+			return SSL.DH_DSS_WITH_AES_128_CBC_SHA;
+		case "DH_RSA_WITH_AES_128_CBC_SHA":
+			return SSL.DH_RSA_WITH_AES_128_CBC_SHA;
+		case "DHE_DSS_WITH_AES_128_CBC_SHA":
+			return SSL.DHE_DSS_WITH_AES_128_CBC_SHA;
+		case "DHE_RSA_WITH_AES_128_CBC_SHA":
+			return SSL.DHE_RSA_WITH_AES_128_CBC_SHA;
+		case "DH_DSS_WITH_AES_256_CBC_SHA":
+			return SSL.DH_DSS_WITH_AES_256_CBC_SHA;
+		case "DH_RSA_WITH_AES_256_CBC_SHA":
+			return SSL.DH_RSA_WITH_AES_256_CBC_SHA;
+		case "DHE_DSS_WITH_AES_256_CBC_SHA":
+			return SSL.DHE_DSS_WITH_AES_256_CBC_SHA;
+		case "DHE_RSA_WITH_AES_256_CBC_SHA":
+			return SSL.DHE_RSA_WITH_AES_256_CBC_SHA;
+		case "DH_DSS_WITH_AES_128_CBC_SHA256":
+			return SSL.DH_DSS_WITH_AES_128_CBC_SHA256;
+		case "DH_RSA_WITH_AES_128_CBC_SHA256":
+			return SSL.DH_RSA_WITH_AES_128_CBC_SHA256;
+		case "DHE_DSS_WITH_AES_128_CBC_SHA256":
+			return SSL.DHE_DSS_WITH_AES_128_CBC_SHA256;
+		case "DHE_RSA_WITH_AES_128_CBC_SHA256":
+			return SSL.DHE_RSA_WITH_AES_128_CBC_SHA256;
+		case "DH_DSS_WITH_AES_256_CBC_SHA256":
+			return SSL.DH_DSS_WITH_AES_256_CBC_SHA256;
+		case "DH_RSA_WITH_AES_256_CBC_SHA256":
+			return SSL.DH_RSA_WITH_AES_256_CBC_SHA256;
+		case "DHE_DSS_WITH_AES_256_CBC_SHA256":
+			return SSL.DHE_DSS_WITH_AES_256_CBC_SHA256;
+		case "DHE_RSA_WITH_AES_256_CBC_SHA256":
+			return SSL.DHE_RSA_WITH_AES_256_CBC_SHA256;
+		case "DH_anon_WITH_RC4_128_MD5":
+			return SSL.DH_anon_WITH_RC4_128_MD5;
+		case "DH_anon_WITH_3DES_EDE_CBC_SHA":
+			return SSL.DH_anon_WITH_3DES_EDE_CBC_SHA;
+		case "DH_anon_WITH_AES_128_CBC_SHA":
+			return SSL.DH_anon_WITH_AES_128_CBC_SHA;
+		case "DH_anon_WITH_AES_256_CBC_SHA":
+			return SSL.DH_anon_WITH_AES_256_CBC_SHA;
+		case "DH_anon_WITH_AES_128_CBC_SHA256":
+			return SSL.DH_anon_WITH_AES_128_CBC_SHA256;
+		case "DH_anon_WITH_AES_256_CBC_SHA256":
+			return SSL.DH_anon_WITH_AES_256_CBC_SHA256;
+		case "ECDH_ECDSA_WITH_NULL_SHA":
+			return SSL.ECDH_ECDSA_WITH_NULL_SHA;
+		case "ECDH_ECDSA_WITH_RC4_128_SHA":
+			return SSL.ECDH_ECDSA_WITH_RC4_128_SHA;
+		case "ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA":
+			return SSL.ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA;
+		case "ECDH_ECDSA_WITH_AES_128_CBC_SHA":
+			return SSL.ECDH_ECDSA_WITH_AES_128_CBC_SHA;
+		case "ECDH_ECDSA_WITH_AES_256_CBC_SHA":
+			return SSL.ECDH_ECDSA_WITH_AES_256_CBC_SHA;
+		case "ECDHE_ECDSA_WITH_NULL_SHA":
+			return SSL.ECDHE_ECDSA_WITH_NULL_SHA;
+		case "ECDHE_ECDSA_WITH_RC4_128_SHA":
+			return SSL.ECDHE_ECDSA_WITH_RC4_128_SHA;
+		case "ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA":
+			return SSL.ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA;
+		case "ECDHE_ECDSA_WITH_AES_128_CBC_SHA":
+			return SSL.ECDHE_ECDSA_WITH_AES_128_CBC_SHA;
+		case "ECDHE_ECDSA_WITH_AES_256_CBC_SHA":
+			return SSL.ECDHE_ECDSA_WITH_AES_256_CBC_SHA;
+		case "ECDH_RSA_WITH_NULL_SHA":
+			return SSL.ECDH_RSA_WITH_NULL_SHA;
+		case "ECDH_RSA_WITH_RC4_128_SHA":
+			return SSL.ECDH_RSA_WITH_RC4_128_SHA;
+		case "ECDH_RSA_WITH_3DES_EDE_CBC_SHA":
+			return SSL.ECDH_RSA_WITH_3DES_EDE_CBC_SHA;
+		case "ECDH_RSA_WITH_AES_128_CBC_SHA":
+			return SSL.ECDH_RSA_WITH_AES_128_CBC_SHA;
+		case "ECDH_RSA_WITH_AES_256_CBC_SHA":
+			return SSL.ECDH_RSA_WITH_AES_256_CBC_SHA;
+		case "ECDHE_RSA_WITH_NULL_SHA":
+			return SSL.ECDHE_RSA_WITH_NULL_SHA;
+		case "ECDHE_RSA_WITH_RC4_128_SHA":
+			return SSL.ECDHE_RSA_WITH_RC4_128_SHA;
+		case "ECDHE_RSA_WITH_3DES_EDE_CBC_SHA":
+			return SSL.ECDHE_RSA_WITH_3DES_EDE_CBC_SHA;
+		case "ECDHE_RSA_WITH_AES_128_CBC_SHA":
+			return SSL.ECDHE_RSA_WITH_AES_128_CBC_SHA;
+		case "ECDHE_RSA_WITH_AES_256_CBC_SHA":
+			return SSL.ECDHE_RSA_WITH_AES_256_CBC_SHA;
+		case "ECDH_anon_WITH_NULL_SHA":
+			return SSL.ECDH_anon_WITH_NULL_SHA;
+		case "ECDH_anon_WITH_RC4_128_SHA":
+			return SSL.ECDH_anon_WITH_RC4_128_SHA;
+		case "ECDH_anon_WITH_3DES_EDE_CBC_SHA":
+			return SSL.ECDH_anon_WITH_3DES_EDE_CBC_SHA;
+		case "ECDH_anon_WITH_AES_128_CBC_SHA":
+			return SSL.ECDH_anon_WITH_AES_128_CBC_SHA;
+		case "ECDH_anon_WITH_AES_256_CBC_SHA":
+			return SSL.ECDH_anon_WITH_AES_256_CBC_SHA;
+		case "RSA_WITH_AES_128_GCM_SHA256":
+			return SSL.RSA_WITH_AES_128_GCM_SHA256;
+		case "RSA_WITH_AES_256_GCM_SHA384":
+			return SSL.RSA_WITH_AES_256_GCM_SHA384;
+		case "DHE_RSA_WITH_AES_128_GCM_SHA256":
+			return SSL.DHE_RSA_WITH_AES_128_GCM_SHA256;
+		case "DHE_RSA_WITH_AES_256_GCM_SHA384":
+			return SSL.DHE_RSA_WITH_AES_256_GCM_SHA384;
+		case "DH_RSA_WITH_AES_128_GCM_SHA256":
+			return SSL.DH_RSA_WITH_AES_128_GCM_SHA256;
+		case "DH_RSA_WITH_AES_256_GCM_SHA384":
+			return SSL.DH_RSA_WITH_AES_256_GCM_SHA384;
+		case "DHE_DSS_WITH_AES_128_GCM_SHA256":
+			return SSL.DHE_DSS_WITH_AES_128_GCM_SHA256;
+		case "DHE_DSS_WITH_AES_256_GCM_SHA384":
+			return SSL.DHE_DSS_WITH_AES_256_GCM_SHA384;
+		case "DH_DSS_WITH_AES_128_GCM_SHA256":
+			return SSL.DH_DSS_WITH_AES_128_GCM_SHA256;
+		case "DH_DSS_WITH_AES_256_GCM_SHA384":
+			return SSL.DH_DSS_WITH_AES_256_GCM_SHA384;
+		case "DH_anon_WITH_AES_128_GCM_SHA256":
+			return SSL.DH_anon_WITH_AES_128_GCM_SHA256;
+		case "DH_anon_WITH_AES_256_GCM_SHA384":
+			return SSL.DH_anon_WITH_AES_256_GCM_SHA384;
+		case "ECDHE_ECDSA_WITH_AES_128_CBC_SHA256":
+			return SSL.ECDHE_ECDSA_WITH_AES_128_CBC_SHA256;
+		case "ECDHE_ECDSA_WITH_AES_256_CBC_SHA384":
+			return SSL.ECDHE_ECDSA_WITH_AES_256_CBC_SHA384;
+		case "ECDH_ECDSA_WITH_AES_128_CBC_SHA256":
+			return SSL.ECDH_ECDSA_WITH_AES_128_CBC_SHA256;
+		case "ECDH_ECDSA_WITH_AES_256_CBC_SHA384":
+			return SSL.ECDH_ECDSA_WITH_AES_256_CBC_SHA384;
+		case "ECDHE_RSA_WITH_AES_128_CBC_SHA256":
+			return SSL.ECDHE_RSA_WITH_AES_128_CBC_SHA256;
+		case "ECDHE_RSA_WITH_AES_256_CBC_SHA384":
+			return SSL.ECDHE_RSA_WITH_AES_256_CBC_SHA384;
+		case "ECDH_RSA_WITH_AES_128_CBC_SHA256":
+			return SSL.ECDH_RSA_WITH_AES_128_CBC_SHA256;
+		case "ECDH_RSA_WITH_AES_256_CBC_SHA384":
+			return SSL.ECDH_RSA_WITH_AES_256_CBC_SHA384;
+		case "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256":
+			return SSL.ECDHE_ECDSA_WITH_AES_128_GCM_SHA256;
+		case "ECDHE_ECDSA_WITH_AES_256_GCM_SHA384":
+			return SSL.ECDHE_ECDSA_WITH_AES_256_GCM_SHA384;
+		case "ECDH_ECDSA_WITH_AES_128_GCM_SHA256":
+			return SSL.ECDH_ECDSA_WITH_AES_128_GCM_SHA256;
+		case "ECDH_ECDSA_WITH_AES_256_GCM_SHA384":
+			return SSL.ECDH_ECDSA_WITH_AES_256_GCM_SHA384;
+		case "ECDHE_RSA_WITH_AES_128_GCM_SHA256":
+			return SSL.ECDHE_RSA_WITH_AES_128_GCM_SHA256;
+		case "ECDHE_RSA_WITH_AES_256_GCM_SHA384":
+			return SSL.ECDHE_RSA_WITH_AES_256_GCM_SHA384;
+		case "ECDH_RSA_WITH_AES_128_GCM_SHA256":
+			return SSL.ECDH_RSA_WITH_AES_128_GCM_SHA256;
+		case "ECDH_RSA_WITH_AES_256_GCM_SHA384":
+			return SSL.ECDH_RSA_WITH_AES_256_GCM_SHA384;
+		case "ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256":
+			return SSL.ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256;
+		case "ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256":
+			return SSL.ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256;
+		case "DHE_RSA_WITH_CHACHA20_POLY1305_SHA256":
+			return SSL.DHE_RSA_WITH_CHACHA20_POLY1305_SHA256;
+		case "PSK_WITH_CHACHA20_POLY1305_SHA256":
+			return SSL.PSK_WITH_CHACHA20_POLY1305_SHA256;
+		case "ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256":
+			return SSL.ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256;
+		case "DHE_PSK_WITH_CHACHA20_POLY1305_SHA256":
+			return SSL.DHE_PSK_WITH_CHACHA20_POLY1305_SHA256;
+		case "RSA_PSK_WITH_CHACHA20_POLY1305_SHA256":
+			return SSL.RSA_PSK_WITH_CHACHA20_POLY1305_SHA256;
+		default:
+			throw new Exception(string.Format(
+				"Unknown cipher suite: '{0}'", s));
+		}
+	}
+
+	int[] GetHashAndSigns()
+	{
+		return GetHashAndSignsByName(
+			JSON.GetStringArray(conf, "hashAndSigns"));
+	}
+
+	internal static int[] GetHashAndSignsByName(string[] ss)
+	{
+		int[] r = new int[ss.Length];
+		for (int i = 0; i < ss.Length; i ++) {
+			r[i] = GetHashAndSignByName(ss[i]);
+		}
+		return r;
+	}
+
+	internal static int GetHashAndSignByName(string s)
+	{
+		switch (s) {
+		case "RSA_MD5":       return SSL.RSA_MD5;
+		case "RSA_SHA1":      return SSL.RSA_SHA1;
+		case "RSA_SHA224":    return SSL.RSA_SHA224;
+		case "RSA_SHA256":    return SSL.RSA_SHA256;
+		case "RSA_SHA384":    return SSL.RSA_SHA384;
+		case "RSA_SHA512":    return SSL.RSA_SHA512;
+		case "ECDSA_MD5":     return SSL.ECDSA_MD5;
+		case "ECDSA_SHA1":    return SSL.ECDSA_SHA1;
+		case "ECDSA_SHA224":  return SSL.ECDSA_SHA224;
+		case "ECDSA_SHA256":  return SSL.ECDSA_SHA256;
+		case "ECDSA_SHA384":  return SSL.ECDSA_SHA384;
+		case "ECDSA_SHA512":  return SSL.ECDSA_SHA512;
+		default:
+			throw new Exception(string.Format(
+				"Unknown hash-and-sign: '{0}'", s));
+		}
+	}
+
+	int[] GetCurves()
+	{
+		return GetCurvesByName(JSON.GetStringArray(conf, "curves"));
+	}
+
+	internal static int[] GetCurvesByName(string[] ss)
+	{
+		int[] r = new int[ss.Length];
+		for (int i = 0; i < ss.Length; i ++) {
+			r[i] = GetCurveByName(ss[i]);
+		}
+		return r;
+	}
+
+	internal static int GetCurveByName(string s)
+	{
+		switch (s) {
+		case "Curve25519":  return SSL.Curve25519;
+		case "NIST_P256":   return SSL.NIST_P256;
+		case "NIST_P384":   return SSL.NIST_P384;
+		case "NIST_P521":   return SSL.NIST_P521;
+		default:
+			throw new Exception(string.Format(
+				"Unknown curve: '{0}'", s));
+		}
+	}
+
+	/*
+	 * RunEnum() builds and runs synthetic tests that exercise all
+	 * combinations of protocol version, cipher suites, curves
+	 * and hash-and-sign. Curves and hash-and-sign are enumerated
+	 * only for ECDHE suites.
+	 */
+	void RunEnum()
+	{
+		RunEnum(true, true);
+		RunEnum(false, true);
+	}
+
+	int RunEnum(bool cmdClient, bool doit)
+	{
+		int count = 0;
+		foreach (int version in versions) {
+			foreach (int suite in cipherSuites) {
+				if (version < SSL.TLS12 && SSL.IsTLS12(suite)) {
+					continue;
+				}
+				if (!SSL.IsECDHE(suite)) {
+					if (doit) {
+						RunEnum(cmdClient,
+							version, suite, -1, -1);
+					}
+					count ++;
+					continue;
+				}
+				bool needRSA = (version >= SSL.TLS12)
+					&& SSL.IsECDHE_RSA(suite);
+				bool needECDSA = (version >= SSL.TLS12)
+					&& SSL.IsECDHE_ECDSA(suite);
+				foreach (int hs in hashAndSigns) {
+					int sa = hs & 0xFF;
+					if (needRSA && sa != SSL.RSA) {
+						continue;
+					}
+					if (needECDSA && sa != SSL.ECDSA) {
+						continue;
+					}
+					foreach (int curve in curves) {
+						if (doit) {
+							RunEnum(cmdClient,
+								version,
+								suite,
+								curve, hs);
+						}
+						count ++;
+					}
+				}
+			}
+		}
+		return count;
+	}
+
+	int ComputeTotalEnum()
+	{
+		return RunEnum(true, false) + RunEnum(false, false);
+	}
+
+	static string TEnumToString(
+		int version, int suite, int curve, int hs)
+	{
+		StringBuilder sb = new StringBuilder();
+		sb.AppendFormat("enum_{0:X4}_{1:X4}", version, suite);
+		if (curve >= 0 && hs >= 0) {
+			sb.AppendFormat("_{0}_{1}", curve, hs);
+		}
+		return sb.ToString();
+	}
+
+	static bool StringToTEnum(string s,
+		out bool cmdClient,
+		out int version, out int suite,
+		out int curve, out int hs)
+	{
+		cmdClient = false;
+		version = -1;
+		suite = -1;
+		curve = -1;
+		hs = -1;
+		s = s.Trim();
+		if (!s.StartsWith("enum_")) {
+			return false;
+		}
+		s = s.Substring(5);
+		if (s.EndsWith("_client")) {
+			cmdClient = true;
+		} else if (s.EndsWith("_server")) {
+			cmdClient = false;
+		} else {
+			return false;
+		}
+		s = s.Substring(0, s.Length - 7);
+		string[] ww = s.Split('_');
+		if (ww.Length != 2 && ww.Length != 4) {
+			return false;
+		}
+		version = ParseHex4(ww[0]);
+		suite = ParseHex4(ww[1]);
+		if (ww.Length == 2) {
+			return version >= 0 && suite >= 0;
+		} else {
+			curve = ParseDec(ww[2]);
+			hs = ParseDec(ww[3]);
+			return version >= 0 && suite >= 0
+				&& curve >= 0 && hs >= 0;
+		}
+	}
+
+	static int HexVal(int cp)
+	{
+		if (cp >= '0' && cp <= '9') {
+			return cp - '0';
+		} else if (cp >= 'A' && cp <= 'F') {
+			return cp - ('A' - 10);
+		} else if (cp >= 'a' && cp <= 'f') {
+			return cp - ('a' - 10);
+		} else {
+			return -1;
+		}
+	}
+
+	static int ParseHex4(string s)
+	{
+		if (s.Length != 4) {
+			return -1;
+		}
+		return ParseHex(s);
+	}
+
+	static int ParseHex(string s)
+	{
+		int acc = 0;
+		for (int i = 0; i < 4; i ++) {
+			int v = HexVal(s[i]);
+			if (v < 0) {
+				return -1;
+			}
+			acc = (acc << 4) + v;
+		}
+		return acc;
+	}
+
+	static int ParseDec(string s)
+	{
+		int n = s.Length;
+		int acc = 0;
+		for (int i = 0; i < n; i ++) {
+			int v = HexVal(s[i]);
+			if (v < 0 || v >= 10) {
+				return -1;
+			}
+			acc = (acc * 10) + v;
+		}
+		return acc;
+	}
+
+	void RunEnum(bool cmdClient, int version, int suite, int curve, int hs)
+	{
+		IDictionary<string, object> d =
+			new SortedDictionary<string, object>(
+				StringComparer.Ordinal);
+		d["name"] = TEnumToString(version, suite, curve, hs);
+		d["versionMin"] = SSL.VersionName(version);
+		d["versionMax"] = SSL.VersionName(version);
+		d["cipherSuites"] = new string[] {
+			SSL.CipherSuiteName(suite)
+		};
+		if (curve >= 0) {
+			d["curves"] = new string[] {
+				SSL.CurveName(curve)
+			};
+			d["hashAndSigns"] = new string[] {
+				SSL.HashAndSignName(hs)
+			};
+		} else {
+			d["curves"] = new string[0];
+			d["hashAndSigns"] = new string[0];
+		}
+		if (SSL.IsRSA(suite) || SSL.IsECDHE_RSA(suite)) {
+			d["serverCertType"] = "RSA";
+		}
+		if (SSL.IsECDH(suite) || SSL.IsECDHE_ECDSA(suite)) {
+			d["serverCertType"] = "EC";
+		}
+		RunTest(cmdClient, d);
+	}
+
+	/*
+	 * Get certificate type for the provided test (client-side or
+	 * server-side certificate, depending on 'client').
+	 *
+	 * If the test does not contain an explicit indication, then
+	 * the certificate type will be "none" for a client, "RSA" for
+	 * a server.
+	 */
+	string GetCertType(object obj, bool client)
+	{
+		string name = client ? "clientCertType" : "serverCertType";
+		string ct;
+		if (JSON.TryGetString(obj, name, out ct)) {
+			return ct;
+		}
+		return client ? "none" : "RSA";
+	}
+
+	int GetNumTests(object obj)
+	{
+		int num = 0;
+		bool v;
+		if (!JSON.TryGetBool(obj, "serverOnly", out v) || !v) {
+			num ++;
+		}
+		if (!JSON.TryGetBool(obj, "clientOnly", out v) || !v) {
+			num ++;
+		}
+		return num;
+	}
+
+	void RunTest(object obj)
+	{
+		bool v;
+		if (!JSON.TryGetBool(obj, "serverOnly", out v) || !v) {
+			RunTest(true, obj);
+		}
+		if (!JSON.TryGetBool(obj, "clientOnly", out v) || !v) {
+			RunTest(false, obj);
+		}
+	}
+
+	void RunTest(bool cmdClient, object obj)
+	{
+		string name = JSON.GetString(obj, "name")
+			+ (cmdClient ? "_client" : "_server");
+		Console.Write("\r({0}/{1})",
+			totalSuccess + totalFailures + 1, totalTests);
+		// Console.Write("{0}:", name);
+
+		/*
+		 * Expected command exit code:
+		 *
+		 *   0 if the command is supposed to exit gracefully
+		 *   1 if the command should detect and report an error
+		 */
+		int expectedExitCode;
+		JSON.TryGetInt32(obj, "expectedExitCode", out expectedExitCode);
+
+		/*
+		 * Expected failure: if defined, then we expect our
+		 * library to throw an exception, and the message should
+		 * contain that specific string.
+		 */
+		string expectedFailure;
+		JSON.TryGetString(obj, "expectedFailure", out expectedFailure);
+
+		/*
+		 * Assemble the sub-process command line:
+		 *
+		 *  - Always one of "-client" or "-server"
+		 *  - For a server command, a certificate and key are
+		 *    always provided (defaults to RSA); for a client,
+		 *    only if explicitly asked for.
+		 */
+		StringBuilder sb = new StringBuilder();
+		if (cmdClient) {
+			sb.Append("-client");
+		} else {
+			sb.Append("-server");
+		}
+		if (commandVerbose) {
+			sb.Append(" -v");
+		}
+		string certType = GetCertType(obj, cmdClient);
+		switch (certType) {
+		case "RSA":
+			sb.AppendFormat(" -cert \"{0}\" -key \"{1}\"",
+				chainRSAFile, skeyRSAFile);
+			break;
+		case "EC":
+			sb.AppendFormat(" -cert \"{0}\" -key \"{1}\"",
+				chainECFile, skeyECFile);
+			break;
+		case "none":
+			break;
+		default:
+			throw new Exception("Unknown certType: " + certType);
+		}
+		string extra;
+		if (JSON.TryGetString(obj, "extraArgs", out extra)) {
+			sb.Append(' ');
+			sb.Append(extra);
+		}
+
+		/*
+		 * Run the sub-process.
+		 */
+		ProcessStartInfo si = new ProcessStartInfo();
+		si.FileName = commandFile;
+		si.Arguments = string.Format(commandArgs, sb.ToString());
+		si.UseShellExecute = false;
+		si.ErrorDialog = false;
+		si.CreateNoWindow = true;
+		si.RedirectStandardInput = true;
+		si.RedirectStandardOutput = true;
+
+		using (Process pp = new Process()) {
+			pp.StartInfo = si;
+			pp.Start();
+			Exception delayed = null;
+			try {
+				/*
+				 * TODO: add a time-out on the streams
+				 * so that the test never stalls
+				 * indefinitely if the two SSL engines
+				 * lose synchronisation.
+				 */
+				MergeStream ms = new MergeStream(
+					pp.StandardOutput.BaseStream,
+					pp.StandardInput.BaseStream);
+				if (trace) {
+					ms.Debug = Console.Out;
+				}
+				RunTestInner(cmdClient, obj, ms);
+			} catch (Exception ex) {
+				delayed = ex;
+			}
+
+			/*
+			 * Once the test has run, we must make sure that
+			 * the sub-processed is finished. It _should_ end
+			 * properly by itself for all successful test cases,
+			 * so if we have to kill it, then it's a bug.
+			 */
+			bool killed = false;
+			if (!pp.WaitForExit(2000)) {
+				try {
+					pp.Kill();
+				} catch {
+					// ignored
+				}
+				pp.WaitForExit();
+				killed = true;
+			}
+			int exc = pp.ExitCode;
+
+			/*
+			 * If we had to kill the command, then that is
+			 * always a bug. Otherwise, we compare what we
+			 * got with the expected outcomes.
+			 */
+			List<string> msg = new List<string>();
+			if (killed) {
+				msg.Add("COMMAND KILLED");
+			}
+			if (exc != expectedExitCode) {
+				msg.Add("Wrong exit code: "
+					+ exc + " (expected: "
+					+ expectedExitCode + ")");
+			}
+			if (delayed == null) {
+				if (expectedFailure != null) {
+					msg.Add("An exception was expected");
+				}
+			} else {
+				if (expectedFailure == null) {
+					msg.Add(delayed.ToString());
+				} else {
+					string s = delayed.Message;
+					if (s == null) {
+						s = "";
+					}
+					if (s.IndexOf(expectedFailure) < 0) {
+						msg.Add(delayed.ToString());
+					}
+				}
+			}
+			if (msg.Count == 0) {
+				totalSuccess ++;
+			} else {
+				Console.WriteLine("{0}: FAIL:", name);
+				foreach (string s in msg) {
+					Console.WriteLine(s);
+				}
+				totalFailures ++;
+			}
+		}
+	}
+
+	void RunTestInner(bool cmdClient, object obj, Stream peer)
+	{
+		/*
+		 * Create the SSL engine, and configure it as specified
+		 * in the configuration object (with the default
+		 * configuration as fallback).
+		 */
+
+		SSLEngine eng;
+		byte[][] chain = null;
+		IPrivateKey skey = null;
+		string certType = GetCertType(obj, !cmdClient);
+		switch (certType) {
+		case "RSA":
+			chain = chainRSA;
+			skey = skeyRSA;
+			break;
+		case "EC":
+			chain = chainEC;
+			skey = skeyEC;
+			break;
+		case "none":
+			break;
+		default:
+			throw new Exception("Unknown certType: " + certType);
+		}
+		if (cmdClient) {
+			IServerPolicy spol = new SSLServerPolicyBasic(
+				chain, skey, KeyUsage.EncryptAndSign);
+			SSLServer ss = new SSLServer(peer, spol);
+			ss.SessionCache = new SSLSessionCacheLRU(20);
+			eng = ss;
+		} else {
+			SSLClient sc = new SSLClient(peer);
+			sc.ServerCertValidator =
+				SSLClient.InsecureCertValidator;
+			eng = sc;
+		}
+		eng.NormalizeIOError = true;
+		eng.AutoFlush = false;
+
+		/*
+		 * Minimum version.
+		 */
+		string svmin;
+		if (JSON.TryGetString(obj, "versionMin", out svmin)) {
+			eng.VersionMin = GetVersionByName(svmin);
+		} else {
+			eng.VersionMin = versionMin;
+		}
+
+		/*
+		 * Maximum version.
+		 */
+		string svmax;
+		if (JSON.TryGetString(obj, "versionMax", out svmax)) {
+			eng.VersionMax = GetVersionByName(svmax);
+		} else {
+			eng.VersionMax = versionMax;
+		}
+
+		/*
+		 * Supported cipher suites.
+		 */
+		string[] sccs;
+		if (JSON.TryGetStringArray(obj, "cipherSuites", out sccs)) {
+			eng.SupportedCipherSuites = GetSuitesByName(sccs);
+		} else {
+			eng.SupportedCipherSuites = cipherSuites;
+		}
+
+		/*
+		 * Supported hash-and-sign algorithms.
+		 */
+		string[] shss;
+		if (JSON.TryGetStringArray(obj, "hashAndSigns", out shss)) {
+			eng.SupportedHashAndSign = GetHashAndSignsByName(shss);
+		} else {
+			eng.SupportedHashAndSign = hashAndSigns;
+		}
+
+		/*
+		 * Supported elliptic curves.
+		 */
+		string[] secc;
+		if (JSON.TryGetStringArray(obj, "curves", out secc)) {
+			eng.SupportedCurves = GetCurvesByName(secc);
+		} else {
+			eng.SupportedCurves = curves;
+		}
+
+		/*
+		 * What to do when there is no close_notify.
+		 */
+		bool ncn;
+		if (JSON.TryGetBool(obj, "noCloseNotify", out ncn)) {
+			eng.NoCloseNotify = ncn;
+		} else {
+			eng.NoCloseNotify = noCloseNotify;
+		}
+
+		/*
+		 * Quirks.
+		 */
+		IDictionary<string, object> qm;
+		if (JSON.TryGetObjectMap(obj, "quirks", out qm)) {
+			SSLQuirks q = new SSLQuirks();
+			foreach (string name in qm.Keys) {
+				q[name] = JSON.GetString(qm, name);
+			}
+			eng.Quirks = q;
+		}
+
+		bool askClose;
+		JSON.TryGetBool(obj, "askClose", out askClose);
+		bool renegotiate, renegotiateAccepted;
+		renegotiate = JSON.TryGetBool(obj, "renegotiate",
+			out renegotiateAccepted);
+		bool askRenegotiate, askRenegotiateAccepted;
+		askRenegotiate = JSON.TryGetBool(obj, "askRenegotiate",
+			out askRenegotiateAccepted);
+
+		bool reconnectSelf = false, reconnectPeer = false;
+		string rcs;
+		if (JSON.TryGetString(obj, "reconnect", out rcs)) {
+			switch (rcs) {
+			case "self": reconnectSelf = true; break;
+			case "peer": reconnectPeer = true; break;
+			default:
+				throw new Exception("Unknown 'reconnect' type: "
+					+ rcs);
+			}
+		}
+
+		bool forgetSelf = false, forgetPeer = false;
+		string fgs;
+		if (JSON.TryGetString(obj, "forget", out fgs)) {
+			switch (fgs) {
+			case "self": forgetSelf = true; break;
+			case "peer": forgetPeer = true; break;
+			default:
+				throw new Exception("Unknown 'forget' type: "
+					+ fgs);
+			}
+		}
+
+		if (askClose) {
+			SendCommand(eng, 'C');
+			if (eng.ReadByte() != -1) {
+				throw new Exception("Peer did not close");
+			}
+		} else if (renegotiate) {
+			SendMessageNormal(eng, 10);
+			if (eng.Renegotiate()) {
+				if (!renegotiateAccepted) {
+					throw new Exception("Renegotiation"
+						+ " should have been rejected");
+				}
+			} else {
+				if (renegotiateAccepted) {
+					throw new Exception("Renegotiation"
+						+ " should have been accepted");
+				}
+			}
+			SendMessageNormal(eng, 9);
+		} else if (askRenegotiate) {
+			SendMessageNormal(eng, 10);
+			long rc = eng.HandshakeCount;
+			SendCommand(eng, 'G');
+			string s = ReadLine(eng);
+			switch (s) {
+			case "DENIED":
+				if (askRenegotiateAccepted) {
+					throw new Exception("Renegotiation"
+						+ " should have been accepted");
+				}
+				break;
+			case "OK":
+				if (!askRenegotiateAccepted) {
+					throw new Exception("Renegotiation"
+						+ " should have been rejected");
+				}
+				long nrc = eng.HandshakeCount;
+				if (nrc != rc + 1) {
+					throw new Exception(string.Format(
+						"Wrong handshake count"
+						+ " (old={0}, new={1})",
+						rc, nrc));
+				}
+				break;
+			default:
+				throw new Exception(string.Format(
+					"Unexpected answer string '{0}'", s));
+			}
+			SendMessageNormal(eng, 8);
+		} else if (reconnectSelf || reconnectPeer) {
+			SendMessageNormal(eng, 50);
+			SendMessageNormal(eng, 100);
+			if (forgetPeer) {
+				SendCommand(eng, 'U');
+				string s = ReadLine(eng);
+				if (s != "DONE") {
+					throw new Exception(string.Format(
+						"Unexpected answer '{0}'", s));
+				}
+			}
+			eng.CloseSub = false;
+			if (reconnectPeer) {
+				SendCommand(eng, 'T');
+				if (eng.ReadByte() != -1) {
+					throw new Exception(
+						"Peer did not close");
+				}
+			} else {
+				SendCommand(eng, 'R');
+				string s = ReadLine(eng);
+				if (s != "OK") {
+					throw new Exception(string.Format(
+						"Unexpected answer '{0}'", s));
+				}
+				eng.Close();
+			}
+			SSLEngine eng2;
+			if (cmdClient) {
+				IServerPolicy spol = new SSLServerPolicyBasic(
+					chain, skey, KeyUsage.EncryptAndSign);
+				SSLServer ss = new SSLServer(peer, spol);
+				if (forgetSelf) {
+					ss.SessionCache =
+						new SSLSessionCacheLRU(20);
+				} else {
+					ss.SessionCache =
+						((SSLServer)eng).SessionCache;
+				}
+				eng2 = ss;
+			} else {
+				SSLSessionParameters sp;
+				if (forgetSelf) {
+					sp = null;
+				} else {
+					sp = eng.SessionParameters;
+				}
+				SSLClient sc = new SSLClient(peer, sp);
+				sc.ServerCertValidator =
+					SSLClient.InsecureCertValidator;
+				eng2 = sc;
+			}
+			eng2.NormalizeIOError = eng.NormalizeIOError;
+			eng2.AutoFlush = eng.AutoFlush;
+			eng2.VersionMin = eng.VersionMin;
+			eng2.VersionMax = eng.VersionMax;
+			eng2.SupportedCipherSuites = eng.SupportedCipherSuites;
+			eng2.SupportedHashAndSign = eng.SupportedHashAndSign;
+			eng2.SupportedCurves = eng.SupportedCurves;
+			eng2.NoCloseNotify = eng.NoCloseNotify;
+			eng2.Quirks = eng.Quirks;
+			eng = eng2;
+			SendMessageNormal(eng, 60);
+			SendMessageNormal(eng, 90);
+			if (forgetSelf || forgetPeer) {
+				if (eng.IsResume) {
+					throw new Exception(
+						"Session was resumed");
+				}
+			} else {
+				if (!eng.IsResume) {
+					throw new Exception(
+						"Session was not resumed");
+				}
+			}
+		} else {
+			for (int i = 0; i <= 38; i ++) {
+				int len;
+				if (i <= 20) {
+					len = i;
+				} else {
+					len = 20 + (1 << (i - 20));
+				}
+				SendMessageNormal(eng, len);
+			}
+		}
+
+		eng.Close();
+	}
+
+	/*
+	 * Send a "normal" message to the peer, of the specified
+	 * length: this is a sequence of 'len' random bytes, distinct
+	 * from 0x0A, followed one 0x0A byte. The peer is supposed to
+	 * respond with the SHA-1 hash of the message bytes (excluding
+	 * the final 0x0A), encoded in hexadecimal (lowercase) and
+	 * followed by a newline (0x0A). An exception is thrown if the
+	 * expected value is not obtained.
+	 */
+	void SendMessageNormal(SSLEngine eng, int len)
+	{
+		SHA1 sha1 = new SHA1();
+		byte[] buf = new byte[len + 1];
+		RNG.GetBytesNonZero(buf, 0, len);
+		for (int i = 0; i < len; i ++) {
+			buf[i] ^= 0x0A;
+		}
+		buf[len] = 0x0A;
+		if (len == 1) {
+			buf[0] = (byte)('a' + (buf[0] & 0x0F));
+		}
+		StringBuilder sb = new StringBuilder();
+		foreach (byte b in sha1.Hash(buf, 0, len)) {
+			sb.AppendFormat("{0:x2}", b);
+		}
+		sb.Append('\n');
+		eng.Write(buf, 0, buf.Length);
+		eng.Flush();
+		for (int i = 0; i < sb.Length; i ++) {
+			int x = eng.ReadByte();
+			int y = sb[i];
+			if (x != y) {
+				throw new Exception(string.Format(
+					"received {0} (exp: {1})", y, x));
+			}
+		}
+	}
+
+	void SendCommand(SSLEngine eng, char cmd)
+	{
+		eng.WriteByte((byte)cmd);
+		eng.WriteByte(0x0A);
+		eng.Flush();
+	}
+
+	string ReadLine(SSLEngine eng)
+	{
+		StringBuilder sb = new StringBuilder();
+		for (;;) {
+			int c = eng.ReadByte();
+			if (c < 0) {
+				throw new Exception("Unexpected EOF");
+			}
+			if (c == 0x0A) {
+				return sb.ToString();
+			}
+			sb.Append((char)c);
+		}
+	}
+
+	static byte[][] DecodeChain(string fname)
+	{
+		byte[] buf = File.ReadAllBytes(fname);
+		PEMObject[] fpo = AsnIO.DecodePEM(buf);
+		if (fpo.Length == 0) {
+			buf = AsnIO.FindBER(buf);
+			if (buf == null) {
+				throw new Exception(string.Format(
+					"No certificate in file '{0}'", fname));
+			}
+			return new byte[][] { buf };
+		}
+		List<byte[]> r = new List<byte[]>();
+		foreach (PEMObject po in fpo) {
+			string tt = po.type.ToUpperInvariant();
+			if (tt == "CERTIFICATE" || tt == "X509 CERTIFICATE") {
+				r.Add(po.data);
+			}
+		}
+		if (r.Count == 0) {
+			throw new Exception(string.Format(
+				"No certificate in file '{0}'", fname));
+		}
+		return r.ToArray();
+	}
+
+	static IPrivateKey DecodePrivateKey(string fname)
+	{
+		byte[] buf = File.ReadAllBytes(fname);
+		PEMObject[] fpo = AsnIO.DecodePEM(buf);
+		if (fpo.Length == 0) {
+			buf = AsnIO.FindBER(buf);
+		} else {
+			buf = null;
+			foreach (PEMObject po in fpo) {
+				string tt = po.type.ToUpperInvariant();
+				if (tt.IndexOf("PRIVATE KEY") >= 0) {
+					if (buf != null) {
+						throw new Exception(
+							"Multiple keys in '"
+							+ fname + "'");
+					}
+					buf = po.data;
+				}
+			}
+		}
+		if (buf == null) {
+			throw new Exception(string.Format(
+				"No private key in file '{0}'", fname));
+		}
+		return KF.DecodePrivateKey(buf);
+	}
+}
diff --git a/X500/DNPart.cs b/X500/DNPart.cs
new file mode 100644
index 0000000..57f02a3
--- /dev/null
+++ b/X500/DNPart.cs
@@ -0,0 +1,550 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+using Asn1;
+
+namespace X500 {
+
+/*
+ * A DNPart instance encodes an X.500 name element: it has a type (an
+ * OID) and a value. The value is an ASN.1 object. If the name type is
+ * one of a list of standard types, then the value is a character
+ * string, and there is a "friendly type" which is a character string
+ * (such as "CN" for the "common name", of OID 2.5.4.3).
+ */
+
+public class DNPart {
+
+	/*
+	 * These are the known "friendly types". The values decode as
+	 * strings.
+	 */
+
+	public const string COMMON_NAME          = "CN";
+	public const string LOCALITY             = "L";
+	public const string STATE                = "ST";
+	public const string ORGANIZATION         = "O";
+	public const string ORGANIZATIONAL_UNIT  = "OU";
+	public const string COUNTRY              = "C";
+	public const string STREET               = "STREET";
+	public const string DOMAIN_COMPONENT     = "DC";
+	public const string USER_ID              = "UID";
+	public const string EMAIL_ADDRESS        = "EMAILADDRESS";
+
+	/*
+	 * Get the type OID (decimal-dotted string representation).
+	 */
+	public string OID {
+		get {
+			return OID_;
+		}
+	}
+	string OID_;
+
+	/*
+	 * Get the string value for this element. If the element value
+	 * could not be decoded as a string, then this method returns
+	 * null.
+	 *
+	 * (Decoding error for name elements of a standard type trigger
+	 * exceptions upon instance creation. Thus, a null value is
+	 * possible only for a name element that uses an unknown type.)
+	 */
+	public string Value {
+		get {
+			return Value_;
+		}
+	}
+	string Value_;
+
+	/*
+	 * Tell whether this element is string based. This property
+	 * returns true if and only if Value returns a non-null value.
+	 */
+	public bool IsString {
+		get {
+			return Value_ != null;
+		}
+	}
+
+	/*
+	 * Get the element value as an ASN.1 structure.
+	 */
+	public AsnElt AsnValue {
+		get {
+			return AsnValue_;
+		}
+	}
+	AsnElt AsnValue_;
+
+	/*
+	 * Get the "friendly type" for this element. This is the
+	 * string mnemonic such as "CN" for "common name". If no
+	 * friendly type is known for that element, then the OID
+	 * is returned (decimal-dotted representation).
+	 */
+	public string FriendlyType {
+		get {
+			return GetFriendlyType(OID);
+		}
+	}
+
+	/*
+	 * "Normalized" string value (converted to uppercase then
+	 * lowercase, leading and trailing whitespace trimmed, adjacent
+	 * spaces coalesced). This should allow for efficient comparison
+	 * while still supporting most corner cases.
+	 *
+	 * This does not implement full RFC 4518 rules, but it should
+	 * be good enough for an analysis tool.
+	 */
+	string normValue;
+
+	byte[] encodedValue;
+	int hashCode;
+
+	internal DNPart(string oid, AsnElt val)
+	{
+		OID_ = oid;
+		AsnValue_ = val;
+		encodedValue = val.Encode();
+		uint hc = (uint)oid.GetHashCode();
+		try {
+			string s = val.GetString();
+			Value_ = s;
+			s = s.ToUpperInvariant().ToLowerInvariant();
+			StringBuilder sb = new StringBuilder();
+			bool lwws = true;
+			foreach (char c in s.Trim()) {
+				if (IsControl(c)) {
+					continue;
+				}
+				if (IsWS(c)) {
+					if (lwws) {
+						continue;
+					}
+					lwws = true;
+					sb.Append(' ');
+				} else {
+					sb.Append(c);
+				}
+			}
+			int n = sb.Length;
+			if (n > 0 && sb[n - 1] == ' ') {
+				sb.Length = n - 1;
+			}
+			normValue = sb.ToString();
+			hc += (uint)normValue.GetHashCode();
+		} catch {
+			if (OID_TO_FT.ContainsKey(oid)) {
+				throw;
+			}
+			Value_ = null;
+			foreach (byte b in encodedValue) {
+				hc = ((hc << 7) | (hc >> 25)) ^ (uint)b;
+			}
+		}
+		hashCode = (int)hc;
+	}
+
+	static bool MustEscape(int x)
+	{
+		if (x < 0x20 || x >= 0x7F) {
+			return true;
+		}
+		switch (x) {
+		case '"':
+		case '+':
+		case ',':
+		case ';':
+		case '<':
+		case '>':
+		case '\\':
+			return true;
+		default:
+			return false;
+		}
+	}
+
+	/*
+	 * Convert this element to a string. This uses RFC 4514 rules.
+	 */
+	public override string ToString()
+	{
+		StringBuilder sb = new StringBuilder();
+		string ft;
+		if (OID_TO_FT.TryGetValue(OID, out ft) && IsString) {
+			sb.Append(ft);
+			sb.Append("=");
+			byte[] buf = Encoding.UTF8.GetBytes(Value);
+			for (int i = 0; i < buf.Length; i ++) {
+				byte b = buf[i];
+				if ((i == 0 && (b == ' ' || b == '#'))
+					|| (i == buf.Length - 1 && b == ' ')
+					|| MustEscape(b))
+				{
+					switch ((char)b) {
+					case ' ':
+					case '"':
+					case '#':
+					case '+':
+					case ',':
+					case ';':
+					case '<':
+					case '=':
+					case '>':
+					case '\\':
+						sb.Append('\\');
+						sb.Append((char)b);
+						break;
+					default:
+						sb.AppendFormat("\\{0:X2}", b);
+						break;
+					}
+				} else {
+					sb.Append((char)b);
+				}
+			}
+		} else {
+			sb.Append(OID);
+			sb.Append("=#");
+			foreach (byte b in AsnValue.Encode()) {
+				sb.AppendFormat("{0:X2}", b);
+			}
+		}
+		return sb.ToString();
+	}
+
+	/*
+	 * Get the friendly type corresponding to the given OID
+	 * (decimal-dotted representation). If no such type is known,
+	 * then the OID string is returned.
+	 */
+	public static string GetFriendlyType(string oid)
+	{
+		string ft;
+		if (OID_TO_FT.TryGetValue(oid, out ft)) {
+			return ft;
+		}
+		return oid;
+	}
+
+	static int HexVal(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);
+		} else {
+			return -1;
+		}
+	}
+
+	static int HexValCheck(char c)
+	{
+		int x = HexVal(c);
+		if (x < 0) {
+			throw new AsnException(String.Format(
+				"Not an hex digit: U+{0:X4}", c));
+		}
+		return x;
+	}
+
+	static int HexVal2(string str, int k)
+	{
+		if (k >= str.Length) {
+			throw new AsnException("Missing hex digits");
+		}
+		int x = HexVal(str[k]);
+		if ((k + 1) >= str.Length) {
+			throw new AsnException("Odd number of hex digits");
+		}
+		return (x << 4) + HexVal(str[k + 1]);
+	}
+
+	static int ReadHexEscape(string str, ref int off)
+	{
+		if (off >= str.Length || str[off] != '\\') {
+			return -1;
+		}
+		if ((off + 1) >= str.Length) {
+			throw new AsnException("Truncated escape");
+		}
+		int x = HexVal(str[off + 1]);
+		if (x < 0) {
+			return -1;
+		}
+		if ((off + 2) >= str.Length) {
+			throw new AsnException("Truncated escape");
+		}
+		int y = HexValCheck(str[off + 2]);
+		off += 3;
+		return (x << 4) + y;
+	}
+
+	static int ReadHexUTF(string str, ref int off)
+	{
+		int x = ReadHexEscape(str, ref off);
+		if (x < 0x80 || x >= 0xC0) {
+			throw new AsnException(
+				"Invalid hex escape: not UTF-8");
+		}
+		return x;
+	}
+
+	static string UnEscapeUTF8(string str)
+	{
+		StringBuilder sb = new StringBuilder();
+		int n = str.Length;
+		int k = 0;
+		while (k < n) {
+			char c = str[k];
+			if (c != '\\') {
+				sb.Append(c);
+				k ++;
+				continue;
+			}
+			int x = ReadHexEscape(str, ref k);
+			if (x < 0) {
+				sb.Append(str[k + 1]);
+				k += 2;
+				continue;
+			}
+			if (x < 0x80) {
+				// nothing
+			} else if (x < 0xC0) {
+				throw new AsnException(
+					"Invalid hex escape: not UTF-8");
+			} else if (x < 0xE0) {
+				x &= 0x1F;
+				x = (x << 6) | ReadHexUTF(str, ref k) & 0x3F;
+			} else if (x < 0xF0) {
+				x &= 0x0F;
+				x = (x << 6) | ReadHexUTF(str, ref k) & 0x3F;
+				x = (x << 6) | ReadHexUTF(str, ref k) & 0x3F;
+			} else if (x < 0xF8) {
+				x &= 0x07;
+				x = (x << 6) | ReadHexUTF(str, ref k) & 0x3F;
+				x = (x << 6) | ReadHexUTF(str, ref k) & 0x3F;
+				x = (x << 6) | ReadHexUTF(str, ref k) & 0x3F;
+				if (x > 0x10FFFF) {
+					throw new AsnException("Invalid"
+						+ " hex escape: out of range");
+				}
+			} else {
+				throw new AsnException(
+					"Invalid hex escape: not UTF-8");
+			}
+			if (x < 0x10000) {
+				sb.Append((char)x);
+			} else {
+				x -= 0x10000;
+				sb.Append((char)(0xD800 + (x >> 10)));
+				sb.Append((char)(0xDC00 + (x & 0x3FF)));
+			}
+		}
+		return sb.ToString();
+	}
+
+	internal static DNPart Parse(string str)
+	{
+		int j = str.IndexOf('=');
+		if (j < 0) {
+			throw new AsnException("Invalid DN: no '=' sign");
+		}
+		string a = str.Substring(0, j).Trim();
+		string b = str.Substring(j + 1).Trim();
+		string oid;
+		if (!FT_TO_OID.TryGetValue(a, out oid)) {
+			oid = AsnElt.MakeOID(oid).GetOID();
+		}
+		AsnElt aVal;
+		if (b.StartsWith("#")) {
+			MemoryStream ms = new MemoryStream();
+			int n = b.Length;
+			for (int k = 1; k < n; k += 2) {
+				int x = HexValCheck(b[k]);
+				if (k + 1 >= n) {
+					throw new AsnException(
+						"Odd number of hex digits");
+				}
+				x = (x << 4) + HexValCheck(b[k + 1]);
+				ms.WriteByte((byte)x);
+			}
+			try {
+				aVal = AsnElt.Decode(ms.ToArray());
+			} catch (Exception e) {
+				throw new AsnException("Bad DN value: "
+					+ e.Message);
+			}
+		} else {
+			b = UnEscapeUTF8(b);
+			int type = AsnElt.PrintableString;
+			foreach (char c in b) {
+				if (!AsnElt.IsPrintable(c)) {
+					type = AsnElt.UTF8String;
+					break;
+				}
+			}
+			aVal = AsnElt.MakeString(type, b);
+		}
+		return new DNPart(oid, aVal);
+	}
+
+	static Dictionary<string, string> OID_TO_FT;
+	static Dictionary<string, string> FT_TO_OID;
+
+	static void AddFT(string oid, string ft)
+	{
+		OID_TO_FT[oid] = ft;
+		FT_TO_OID[ft] = oid;
+	}
+
+	static DNPart()
+	{
+		OID_TO_FT = new Dictionary<string, string>();
+		FT_TO_OID = new Dictionary<string, string>(
+			StringComparer.OrdinalIgnoreCase);
+		AddFT("2.5.4.3", COMMON_NAME);
+		AddFT("2.5.4.7", LOCALITY);
+		AddFT("2.5.4.8", STATE);
+		AddFT("2.5.4.10", ORGANIZATION);
+		AddFT("2.5.4.11", ORGANIZATIONAL_UNIT);
+		AddFT("2.5.4.6", COUNTRY);
+		AddFT("2.5.4.9", STREET);
+		AddFT("0.9.2342.19200300.100.1.25", DOMAIN_COMPONENT);
+		AddFT("0.9.2342.19200300.100.1.1", USER_ID);
+		AddFT("1.2.840.113549.1.9.1", EMAIL_ADDRESS);
+
+		/*
+		 * We also accept 'S' as an alias for 'ST' because some
+		 * Microsoft software uses it.
+		 */
+		FT_TO_OID["S"] = FT_TO_OID["ST"];
+	}
+
+	/*
+	 * Tell whether a given character is a "control character" (to
+	 * be ignored for DN comparison purposes). This follows RFC 4518
+	 * but only for code points in the first plane.
+	 */
+	static bool IsControl(char c)
+	{
+		if (c <= 0x0008
+			|| (c >= 0x000E && c <= 0x001F)
+			|| (c >= 0x007F && c <= 0x0084)
+			|| (c >= 0x0086 && c <= 0x009F)
+			|| c == 0x06DD
+			|| c == 0x070F
+			|| c == 0x180E
+			|| (c >= 0x200C && c <= 0x200F)
+			|| (c >= 0x202A && c <= 0x202E)
+			|| (c >= 0x2060 && c <= 0x2063)
+			|| (c >= 0x206A && c <= 0x206F)
+			|| c == 0xFEFF
+			|| (c >= 0xFFF9 && c <= 0xFFFB))
+		{
+			return true;
+		}
+		return false;
+	}
+
+	/*
+	 * Tell whether a character is whitespace. This follows
+	 * rules of RFC 4518.
+	 */
+	static bool IsWS(char c)
+	{
+		if (c == 0x0020
+			|| c == 0x00A0
+			|| c == 0x1680
+			|| (c >= 0x2000 && c <= 0x200A)
+			|| c == 0x2028
+			|| c == 0x2029
+			|| c == 0x202F
+			|| c == 0x205F
+			|| c == 0x3000)
+		{
+			return true;
+		}
+		return false;
+	}
+
+	public override bool Equals(object obj)
+	{
+		return Equals(obj as DNPart);
+	}
+
+	public bool Equals(DNPart dnp)
+	{
+		if (dnp == null) {
+			return false;
+		}
+		if (OID != dnp.OID) {
+			return false;
+		}
+		if (IsString) {
+			return dnp.IsString
+				&& normValue == dnp.normValue;
+		} else if (dnp.IsString) {
+			return false;
+		} else {
+			return Eq(encodedValue, dnp.encodedValue);
+		}
+	}
+
+	public override int GetHashCode()
+	{
+		return hashCode;
+	}
+
+	static bool Eq(byte[] a, byte[] b)
+	{
+		if (a == b) {
+			return true;
+		}
+		if (a == null || b == null) {
+			return false;
+		}
+		int n = a.Length;
+		if (n != b.Length) {
+			return false;
+		}
+		for (int i = 0; i < n; i ++) {
+			if (a[i] != b[i]) {
+				return false;
+			}
+		}
+		return true;
+	}
+}
+
+}
diff --git a/X500/X500Name.cs b/X500/X500Name.cs
new file mode 100644
index 0000000..438faa6
--- /dev/null
+++ b/X500/X500Name.cs
@@ -0,0 +1,343 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+using Asn1;
+
+namespace X500 {
+
+/*
+ * An X.500 name is a "distinguished name", which is an ordered sequence
+ * of RDN (relative distinguished names). Each RDN is an unordered set
+ * of name elements. A name element is an arbitrary value with an
+ * identifying OID; some (most) name element values are character
+ * strings.
+ *
+ * X.500 names are used primarily to identify certificate owner entities
+ * (subject and issuer name in a certificate), and to serve as
+ * hierarchical indexing key for values in LDAP.
+ *
+ * An X.500 name is encoded and decoded using ASN.1. They can also be
+ * converted to a string representation. The string representation does
+ * not conserve all encoding details, so encoding+parsing will not
+ * necessarily restore the exact same binary DN.
+ */
+
+public class X500Name {
+
+	/*
+	 * Get the individual name elements, in a "flattened" structure
+	 * (if a SET in a RDN contains multiple values, then they are
+	 * stored consecutively in that array).
+	 *
+	 * The returned array MUST NOT be modified.
+	 */
+	public DNPart[] Parts {
+		get {
+			return Parts_;
+		}
+	}
+	DNPart[] Parts_;
+
+	/*
+	 * Get the individual name elements. Each internal array contains
+	 * the name elements found in the SET for a specific RDN.
+	 */
+	public DNPart[][] PartsGen {
+		get {
+			return PartsGen_;
+		}
+	}
+	DNPart[][] PartsGen_;
+
+	/*
+	 * Check whether this DN is empty.
+	 */
+	public bool IsEmpty {
+		get {
+			return Parts_.Length == 0;
+		}
+	}
+
+	int hashCode;
+
+	/*
+	 * Constructor for parsing.
+	 */
+	X500Name(List<List<DNPart>> dn)
+	{
+		Init(dn);
+	}
+
+	void Init(List<List<DNPart>> dn)
+	{
+		int n = dn.Count;
+		List<DNPart> r = new List<DNPart>();
+		PartsGen_ = new DNPart[n][];
+		for (int i = 0; i < n; i ++) {
+			IDictionary<string, DNPart> dd =
+				new SortedDictionary<string, DNPart>(
+					StringComparer.Ordinal);
+			foreach (DNPart dnp in dn[i]) {
+				string nt = dnp.OID;
+				if (dd.ContainsKey(nt)) {
+					throw new AsnException(string.Format(
+						"multiple values of type {0}"
+						+ " in RDN", nt));
+				}
+				dd[nt] = dnp;
+			}
+			PartsGen_[i] = new DNPart[dd.Count];
+			int j = 0;
+			foreach (DNPart p in dd.Values) {
+				PartsGen_[i][j ++] = p;
+				r.Add(p);
+			}
+		}
+		Parts_ = r.ToArray();
+
+		uint hc = 0;
+		foreach (DNPart dnp in r) {
+			hc = ((hc << 7) | (hc >> 25)) + (uint)dnp.GetHashCode();
+		}
+		hashCode = (int)hc;
+	}
+
+	/*
+	 * Simplified parsing: this constructor checks that every SET
+	 * in the sequence of RDN has size exactly 1, and decodes each
+	 * name element as a "generic string".
+	 *
+	 * On decoding error, an AsnException is thrown.
+	 */
+	public X500Name(AsnElt aDN) : this(aDN, true)
+	{
+	}
+
+	/*
+	 * Generic parsing. If 'strictStrings' is true, then the following
+	 * rules are enforced:
+	 * -- Every SET in the sequence of RDN must have size 1.
+	 * -- Every name element is decoded as a string (by tag).
+	 *
+	 * If 'strictStrings' is false, then multiple elements may appear
+	 * in each SET, and values needs not be decodable as string (values
+	 * with a known OID must still be decodable).
+	 *
+	 * This constructor checks that within a single RDN, no two
+	 * attributes may have the same type.
+	 *
+	 * On decoding error, an AsnException is thrown.
+	 */
+	public X500Name(AsnElt aDN, bool strictStrings)
+	{
+		/*
+		 * Note: the SEQUENCE tag MUST be present, since the
+		 * ASN.1 definition of Name starts with a CHOICE; thus,
+		 * any tag override would have to be explicit, not
+		 * implicit.
+		 */
+		aDN.CheckConstructed();
+		aDN.CheckTag(AsnElt.SEQUENCE);
+		List<List<DNPart>> r = new List<List<DNPart>>();
+		foreach (AsnElt aRDN in aDN.Sub) {
+			aRDN.CheckConstructed();
+			aRDN.CheckTag(AsnElt.SET);
+			aRDN.CheckNumSubMin(1);
+			int n = aRDN.Sub.Length;
+			if (n != 1 && strictStrings) {
+				throw new AsnException(String.Format(
+					"several ({0}) values in RDN", n));
+			}
+			List<DNPart> r2 = new List<DNPart>();
+			r.Add(r2);
+			for (int i = 0; i < n; i ++) {
+				AsnElt aTV = aRDN.Sub[i];
+				aTV.CheckConstructed();
+				aTV.CheckTag(AsnElt.SEQUENCE);
+				aTV.CheckNumSub(2);
+				AsnElt aOID = aTV.GetSub(0);
+				aOID.CheckTag(AsnElt.OBJECT_IDENTIFIER);
+				AsnElt aVal = aTV.GetSub(1);
+				string nt = aOID.GetOID();
+				DNPart dnp = new DNPart(nt, aVal);
+				if (strictStrings && !dnp.IsString) {
+					throw new AsnException(
+						"RDN is not a string");
+				}
+				r2.Add(dnp);
+			}
+		}
+		Init(r);
+	}
+
+	/*
+	 * Encode this DN into a string as specified in RFC 4514.
+	 */
+	public override string ToString()
+	{
+		StringBuilder sb = new StringBuilder();
+		for (int i = PartsGen_.Length - 1; i >= 0; i --) {
+			DNPart[] dd = PartsGen_[i];
+			for (int j = 0; j < dd.Length; j ++) {
+				if (j > 0) {
+					sb.Append("+");
+				} else if (sb.Length > 0) {
+					sb.Append(",");
+				}
+				sb.Append(dd[j].ToString());
+			}
+		}
+		return sb.ToString();
+	}
+
+	/*
+	 * Encode back this DN into an ASN.1 structure.
+	 */
+	public AsnElt ToAsn1()
+	{
+		AsnElt[] t1 = new AsnElt[PartsGen_.Length];
+		for (int i = 0; i < PartsGen_.Length; i ++) {
+			DNPart[] dp = PartsGen_[i];
+			AsnElt[] t2 = new AsnElt[dp.Length];
+			for (int j = 0; j < dp.Length; j ++) {
+				t2[j] = AsnElt.Make(AsnElt.SEQUENCE,
+					AsnElt.MakeOID(dp[j].OID),
+					dp[j].AsnValue);
+			}
+			t1[i] = AsnElt.MakeSetOf(t2);
+		}
+		return AsnElt.Make(AsnElt.SEQUENCE, t1);
+	}
+
+	/*
+	 * Parse a string into a DN. The input is expected to use
+	 * RFC 4514 format. Name elements that are provided as
+	 * character strings will be mapped to ASN.1 PrintableString
+	 * values (if they are compatible with that string type)
+	 * or UTF8String values (otherwise).
+	 *
+	 * On parse error, an AsnException is thrown.
+	 */
+	public static X500Name Parse(string str)
+	{
+		int n = str.Length;
+		int p = 0;
+		bool acc = false;
+		List<List<DNPart>> dn = new List<List<DNPart>>();
+		while (p < n) {
+			/*
+			 * Find the next unescaped '+' or ',' sign.
+			 */
+			bool lcwb = false;
+			int q;
+			for (q = p; q < n; q ++) {
+				if (lcwb) {
+					lcwb = false;
+					continue;
+				}
+				switch (str[q]) {
+				case ',':
+				case '+':
+					goto found;
+				case '\\':
+					lcwb = true;
+					break;
+				}
+			}
+		found:
+
+			/*
+			 * Parse DN element.
+			 */
+			DNPart dnp = DNPart.Parse(str.Substring(p, q - p));
+			if (acc) {
+				dn[dn.Count - 1].Add(dnp);
+			} else {
+				List<DNPart> r = new List<DNPart>();
+				r.Add(dnp);
+				dn.Add(r);
+			}
+
+			p = q + 1;
+			acc = q < n && str[q] == '+';
+		}
+
+		dn.Reverse();
+		return new X500Name(dn);
+	}
+
+	/*
+	 * Compare two DN for equality. "null" is equal to "null" but
+	 * to nothing else.
+	 */
+	public static bool Equals(X500Name dn1, X500Name dn2)
+	{
+		if (dn1 == null) {
+			return dn2 == null;
+		} else {
+			return dn1.Equals(dn2);
+		}
+	}
+
+	public override bool Equals(object obj)
+	{
+		return Equals(obj as X500Name);
+	}
+
+	public bool Equals(X500Name dn)
+	{
+		if (dn == null) {
+			return false;
+		}
+		int n = PartsGen.Length;
+		if (dn.PartsGen.Length != n) {
+			return false;
+		}
+		for (int i = 0; i < n; i ++) {
+			DNPart[] p1 = PartsGen[i];
+			DNPart[] p2 = dn.PartsGen[i];
+			int k = p1.Length;
+			if (k != p2.Length) {
+				return false;
+			}
+			for (int j = 0; j < k; j ++) {
+				if (!p1[j].Equals(p2[j])) {
+					return false;
+				}
+			}
+		}
+		return true;
+	}
+
+	public override int GetHashCode()
+	{
+		return hashCode;
+	}
+}
+
+}
diff --git a/XKeys/AlgorithmIdentifier.cs b/XKeys/AlgorithmIdentifier.cs
new file mode 100644
index 0000000..2d76e76
--- /dev/null
+++ b/XKeys/AlgorithmIdentifier.cs
@@ -0,0 +1,120 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+
+using Asn1;
+
+/*
+ * A wrapper class for an AlgorithmIdentifier (SEQUENCE of an OID
+ * then optional parameters).
+ */
+
+class AlgorithmIdentifier {
+
+	/*
+	 * Get the OID that identifies the algorithm.
+	 */
+	internal string OID {
+		get {
+			return oid;
+		}
+	}
+
+	/*
+	 * Get the algorithm parameters. This may be null if the
+	 * structure did not contain parameters.
+	 */
+	internal AsnElt Parameters {
+		get {
+			return parameters;
+		}
+	}
+
+	string oid;
+	AsnElt parameters;
+
+	/*
+	 * Create an instance over the provided ASN.1 element. The
+	 * outer tag will be checked to match the universal tag for
+	 * SEQUENCE.
+	 */
+	internal AlgorithmIdentifier(AsnElt ai) : this(ai, true)
+	{
+	}
+
+	/*
+	 * Create an instance over the provided ASN.1 element. If
+	 * 'checkTag' is true, then the outer tag will be checked to
+	 * match the universal tag for SEQUENCE. Set 'checkTag' to
+	 * false if the tag was already checked, or if it has been
+	 * overwritten with an implicit tag.
+	 */
+	internal AlgorithmIdentifier(AsnElt ai, bool checkTag)
+	{
+		if (checkTag) {
+			ai.CheckTag(AsnElt.SEQUENCE);
+		}
+		ai.CheckNumSubMin(1);
+		ai.CheckNumSubMax(2);
+		AsnElt ao = ai.GetSub(0);
+		ao.CheckTag(AsnElt.OBJECT_IDENTIFIER);
+		oid = ao.GetOID();
+		if (ai.Sub.Length >= 2) {
+			parameters = ai.GetSub(1);
+		} else {
+			parameters = null;
+		}
+	}
+
+	/*
+	 * Create a new instance for a given OID, with no parameters.
+	 */
+	internal AlgorithmIdentifier(string oid) : this(oid, null)
+	{
+	}
+
+	/*
+	 * Create a new instance for a given OID, with the provided
+	 * parameters (which may be null).
+	 */
+	internal AlgorithmIdentifier(string oid, AsnElt parameters)
+	{
+		this.oid = oid;
+		this.parameters = parameters;
+	}
+
+	/*
+	 * Encode this instance as a new ASN.1 object.
+	 */
+	internal AsnElt ToAsn1()
+	{
+		AsnElt ao = AsnElt.MakeOID(oid);
+		if (parameters == null) {
+			return AsnElt.Make(AsnElt.SEQUENCE, ao);
+		} else {
+			return AsnElt.Make(AsnElt.SEQUENCE, ao, parameters);
+		}
+	}
+}
diff --git a/XKeys/KF.cs b/XKeys/KF.cs
new file mode 100644
index 0000000..2ce1100
--- /dev/null
+++ b/XKeys/KF.cs
@@ -0,0 +1,650 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.IO;
+using System.Text;
+
+using Asn1;
+using Crypto;
+
+namespace XKeys {
+
+/*
+ * The KF class contains static methods to decode and encode algorithm
+ * parameters, public keys and private keys.
+ */
+
+public class KF {
+
+	const string OID_RSA = "1.2.840.113549.1.1.1";
+	const string OID_RSA_OAEP = "1.2.840.113549.1.1.7";
+	const string OID_RSA_PSS = "1.2.840.113549.1.1.10";
+	const string OID_DSA = "1.2.840.10040.4.1";
+	const string OID_EC = "1.2.840.10045.2.1";
+
+	/*
+	 * Encode the private key. If 'pk8' is true, then this uses
+	 * PKCS#8 format (unencrypted); otherwise, it uses the
+	 * "internal" format that does not specifically identify the key
+	 * type.
+	 */
+	public static byte[] EncodePrivateKey(IPrivateKey sk, bool pk8)
+	{
+		RSAPrivateKey rk = sk as RSAPrivateKey;
+		/* disabled DSA
+		DSAPrivateKey dk = sk as DSAPrivateKey;
+		*/
+		ECPrivateKey ek = sk as ECPrivateKey;
+		if (rk != null) {
+			AsnElt ark = AsnElt.Make(AsnElt.SEQUENCE,
+				AsnElt.MakeInteger(0),
+				AsnElt.MakeInteger(rk.N),
+				AsnElt.MakeInteger(rk.E),
+				AsnElt.MakeInteger(rk.D),
+				AsnElt.MakeInteger(rk.P),
+				AsnElt.MakeInteger(rk.Q),
+				AsnElt.MakeInteger(rk.DP),
+				AsnElt.MakeInteger(rk.DQ),
+				AsnElt.MakeInteger(rk.IQ));
+			byte[] enc = ark.Encode();
+			if (pk8) {
+				AsnElt apk8 = AsnElt.Make(AsnElt.SEQUENCE,
+					AsnElt.MakeInteger(0),
+					AsnElt.Make(AsnElt.SEQUENCE,
+						AsnElt.MakeOID(OID_RSA),
+						AsnElt.NULL_V),
+					AsnElt.MakeBlob(enc));
+				enc = apk8.Encode();
+			}
+			return enc;
+		/* disabled DSA
+		} else if (dk != null) {
+			if (pk8) {
+				AsnElt adx = AsnElt.MakeInteger(dk.X);
+				AsnElt adp = AsnElt.Make(AsnElt.SEQUENCE,
+					AsnElt.MakeInteger(dk.P),
+					AsnElt.MakeInteger(dk.Q),
+					AsnElt.MakeInteger(dk.G));
+				AsnElt apk8 = AsnElt.Make(AsnElt.SEQUENCE,
+					AsnElt.MakeInteger(0),
+					AsnElt.Make(AsnElt.SEQUENCE,
+						AsnElt.MakeOID(OID_DSA),
+						adp),
+					AsnElt.MakeBlob(adx.Encode()));
+				return apk8.Encode();
+			} else {
+				AsnElt adk = AsnElt.Make(AsnElt.SEQUENCE,
+					AsnElt.MakeInteger(0),
+					AsnElt.MakeInteger(dk.P),
+					AsnElt.MakeInteger(dk.Q),
+					AsnElt.MakeInteger(dk.G),
+					AsnElt.MakeInteger(dk.PublicKey.Y),
+					AsnElt.MakeInteger(dk.X));
+				return adk.Encode();
+			}
+		*/
+		} else if (ek != null) {
+			/*
+			 * The ECPrivateKey class guarantees that the
+			 * private key X is already encoded with the same
+			 * length as the subgroup order.
+			 * The ECPublicKey class provides the public key
+			 * as an already encoded point.
+			 */
+			AsnElt acc = AsnElt.MakeOID(CurveToOID(ek.Curve));
+			AsnElt apv = AsnElt.MakeExplicit(AsnElt.CONTEXT, 1,
+				AsnElt.MakeBitString(ek.PublicKey.Pub));
+			if (pk8) {
+				AsnElt aek = AsnElt.Make(AsnElt.SEQUENCE,
+					AsnElt.MakeInteger(1),
+					AsnElt.MakeBlob(ek.X),
+					apv);
+				AsnElt apk8 = AsnElt.Make(AsnElt.SEQUENCE,
+					AsnElt.MakeInteger(0),
+					AsnElt.Make(AsnElt.SEQUENCE,
+						AsnElt.MakeOID(OID_EC),
+						acc),
+					AsnElt.MakeBlob(aek.Encode()));
+				return apk8.Encode();
+			} else {
+				AsnElt aek = AsnElt.Make(AsnElt.SEQUENCE,
+					AsnElt.MakeInteger(1),
+					AsnElt.MakeBlob(ek.X),
+					AsnElt.MakeExplicit(
+						AsnElt.CONTEXT, 0, acc),
+					apv);
+				return aek.Encode();
+			}
+		} else {
+			if (sk == null) {
+				throw new NullReferenceException();
+			}
+			throw new ArgumentException("Cannot encode "
+				+ sk.AlgorithmName + " private key");
+		}
+	}
+
+	/*
+	 * Encode the private key into a PEM object. If 'pk8' is true,
+	 * then unencrypted PKCS#8 format is used, and the PEM header
+	 * is "BEGIN PRIVATE KEY"; otherwise, the "internal" private key
+	 * format is used, and the PEM header identifies the key type.
+	 *
+	 * If 'crlf' is true, then PEM lines end with CR+LF; otherwise,
+	 * they end with LF only.
+	 */
+	public static string EncodePrivateKeyPEM(IPrivateKey sk,
+		bool pk8, bool crlf)
+	{
+		byte[] enc = EncodePrivateKey(sk, pk8);
+		string objType;
+		if (pk8) {
+			objType = "PRIVATE KEY";
+		} else {
+			objType = sk.AlgorithmName + " PRIVATE KEY";
+		}
+		return ToPEM(objType, enc, crlf ? "\r\n" : "\n");
+	}
+
+	/*
+	 * Decode the provided private key. This method accepts both
+	 * PKCS#8 and the "internal" format; the source object may be
+	 * raw DER, Base64-encoded DER, or PEM. The key type is
+	 * automatically detected.
+	 */
+	public static IPrivateKey DecodePrivateKey(byte[] enc)
+	{
+		string pemType;
+		enc = AsnIO.FindBER(enc, false, out pemType);
+		if (enc == null) {
+			throw new AsnException("Not an encoded object");
+		}
+		AsnElt ak = AsnElt.Decode(enc);
+		ak.CheckConstructed();
+		if (pemType != null) {
+			switch (pemType) {
+			case "RSA PRIVATE KEY":
+				return DecodePrivateKeyRSA(ak);
+			/* disabled DSA
+			case "DSA PRIVATE KEY":
+				return DecodePrivateKeyDSA(ak);
+			*/
+			case "EC PRIVATE KEY":
+				return DecodePrivateKeyEC(ak);
+			case "PRIVATE KEY":
+				return DecodePrivateKeyPKCS8(ak);
+			default:
+				throw new AsnException(
+					"Unknown PEM object: " + pemType);
+			}
+		}
+		if (ak.Sub.Length == 3
+			&& ak.GetSub(0).TagValue == AsnElt.INTEGER
+			&& ak.GetSub(1).TagValue == AsnElt.SEQUENCE
+			&& ak.GetSub(2).TagValue == AsnElt.OCTET_STRING)
+		{
+			return DecodePrivateKeyPKCS8(ak);
+		}
+		if (ak.Sub.Length >= 9) {
+			bool mayBeRSA = true;
+			for (int i = 0; i < 9; i ++) {
+				if (ak.GetSub(i).TagValue != AsnElt.INTEGER) {
+					mayBeRSA = false;
+					break;
+				}
+			}
+			if (mayBeRSA) {
+				return DecodePrivateKeyRSA(ak);
+			}
+		}
+		/* disabled DSA
+		if (ak.Sub.Length >= 6) {
+			bool mayBeDSA = true;
+			for (int i = 0; i < 6; i ++) {
+				if (ak.GetSub(i).TagValue != AsnElt.INTEGER) {
+					mayBeDSA = false;
+					break;
+				}
+			}
+			if (mayBeDSA) {
+				return DecodePrivateKeyDSA(ak);
+			}
+		}
+		*/
+		if (ak.Sub.Length >= 2
+			&& ak.GetSub(0).TagValue == AsnElt.INTEGER
+			&& ak.GetSub(1).TagValue == AsnElt.OCTET_STRING)
+		{
+			return DecodePrivateKeyEC(ak);
+		}
+		throw new AsnException("Unrecognized private key format");
+	}
+
+	static RSAPrivateKey DecodePrivateKeyRSA(AsnElt ak)
+	{
+		ak.CheckNumSubMin(9);
+		ak.GetSub(0).CheckTag(AsnElt.INTEGER);
+		long kt = ak.GetSub(0).GetInteger();
+		if (kt != 0) {
+			throw new AsnException(
+				"Unsupported RSA key type: " + kt);
+		}
+		ak.CheckNumSub(9);
+		return new RSAPrivateKey(
+			GetPositiveInteger(ak.GetSub(1)),
+			GetPositiveInteger(ak.GetSub(2)),
+			GetPositiveInteger(ak.GetSub(3)),
+			GetPositiveInteger(ak.GetSub(4)),
+			GetPositiveInteger(ak.GetSub(5)),
+			GetPositiveInteger(ak.GetSub(6)),
+			GetPositiveInteger(ak.GetSub(7)),
+			GetPositiveInteger(ak.GetSub(8)));
+	}
+
+	/* disabled DSA
+	static DSAPrivateKey DecodePrivateKeyDSA(AsnElt ak)
+	{
+		ak.CheckNumSubMin(6);
+		for (int i = 0; i < 6; i ++) {
+			ak.GetSub(i).CheckTag(AsnElt.INTEGER);
+		}
+		long kt = ak.GetSub(0).GetInteger();
+		if (kt != 0) {
+			throw new AsnException(
+				"Unsupported DSA key type: " + kt);
+		}
+		DSAPrivateKey dsk = new DSAPrivateKey(
+			GetPositiveInteger(ak.GetSub(1)),
+			GetPositiveInteger(ak.GetSub(2)),
+			GetPositiveInteger(ak.GetSub(3)),
+			GetPositiveInteger(ak.GetSub(5)));
+		DSAPublicKey dpk = dsk.PublicKey;
+		if (BigInt.Compare(dpk.Y,
+			GetPositiveInteger(ak.GetSub(4))) != 0)
+		{
+			throw new CryptoException(
+				"DSA key pair public/private mismatch");
+		}
+		return dsk;
+	}
+	*/
+
+	static ECPrivateKey DecodePrivateKeyEC(AsnElt ak)
+	{
+		return DecodePrivateKeyEC(ak, null);
+	}
+
+	static ECPrivateKey DecodePrivateKeyEC(AsnElt ak, ECCurve curve)
+	{
+		ak.CheckNumSubMin(2);
+		ak.GetSub(0).CheckTag(AsnElt.INTEGER);
+		ak.GetSub(1).CheckTag(AsnElt.OCTET_STRING);
+		long kt = ak.GetSub(0).GetInteger();
+		if (kt != 1) {
+			throw new AsnException(
+				"Unsupported EC key type: " + kt);
+		}
+		byte[] x = ak.GetSub(1).CopyValue();
+		byte[] pub = null;
+		int n = ak.Sub.Length;
+		int p = 2;
+		if (p < n) {
+			AsnElt acc = ak.GetSub(p);
+			if (acc.TagClass == AsnElt.CONTEXT
+				&& acc.TagValue == 0)
+			{
+				acc.CheckNumSub(1);
+				acc = acc.GetSub(0);
+				ECCurve curve2 = DecodeCurve(acc);
+
+				/*
+				 * Here, we support only named curves.
+				 */
+				/* obsolete
+				*/
+				if (curve == null) {
+					curve = curve2;
+				} else if (!curve.Equals(curve2)) {
+					throw new AsnException(string.Format(
+						"Inconsistent curve"
+						+ " specification ({0} / {1})",
+						curve.Name, curve2.Name));
+				}
+
+				p ++;
+			}
+		}
+		if (p < n) {
+			AsnElt acc = ak.GetSub(p);
+			if (acc.TagClass == AsnElt.CONTEXT
+				&& acc.TagValue == 1)
+			{
+				acc.CheckNumSub(1);
+				acc = acc.GetSub(0);
+				acc.CheckTag(AsnElt.BIT_STRING);
+				pub = acc.GetBitString();
+			}
+		}
+
+		if (curve == null) {
+			throw new AsnException("No curve specified for EC key");
+		}
+		ECPrivateKey esk = new ECPrivateKey(curve, x);
+		if (pub != null) {
+			ECPublicKey epk = new ECPublicKey(curve, pub);
+			if (!epk.Equals(esk.PublicKey)) {
+				throw new CryptoException(
+					"EC key pair public/private mismatch");
+			}
+		}
+		return esk;
+	}
+
+	static ECCurve DecodeCurve(AsnElt acc)
+	{
+		/*
+		 * We support only named curves for now. PKIX does not
+		 * want to see any other kind of curve anyway (see RFC
+		 * 5480).
+		 */
+		acc.CheckTag(AsnElt.OBJECT_IDENTIFIER);
+		string oid = acc.GetOID();
+		return OIDToCurve(oid);
+	}
+
+	static IPrivateKey DecodePrivateKeyPKCS8(AsnElt ak)
+	{
+		ak.CheckNumSub(3);
+		ak.GetSub(0).CheckTag(AsnElt.INTEGER);
+		long v = ak.GetSub(0).GetInteger();
+		if (v != 0) {
+			throw new AsnException(
+				"Unsupported PKCS#8 version: " + v);
+		}
+		AsnElt aai = ak.GetSub(1);
+		aai.CheckTag(AsnElt.SEQUENCE);
+		aai.CheckNumSubMin(1);
+		aai.CheckNumSubMin(2);
+		aai.GetSub(0).CheckTag(AsnElt.OBJECT_IDENTIFIER);
+		string oid = aai.GetSub(0).GetOID();
+		ak.GetSub(2).CheckTag(AsnElt.OCTET_STRING);
+		byte[] rawKey = ak.GetSub(2).CopyValue();
+		AsnElt ark = AsnElt.Decode(rawKey);
+
+		switch (oid) {
+		case OID_RSA:
+		case OID_RSA_OAEP:
+		case OID_RSA_PSS:
+			return DecodePrivateKeyRSA(ark);
+		/* disabled DSA
+		case OID_DSA:
+			return DecodePrivateKeyDSA(ark);
+		*/
+		case OID_EC:
+			/*
+			 * For elliptic curves, the parameters may
+			 * include the curve specification.
+			 */
+			ECCurve curve = (aai.Sub.Length == 2)
+				? DecodeCurve(aai.GetSub(1)) : null;
+			return DecodePrivateKeyEC(ark, curve);
+		default:
+			throw new AsnException(
+				"Unknown PKCS#8 key type: " + oid);
+		}
+	}
+
+	/*
+	 * Encode a public key as a SubjectPublicKeyInfo structure.
+	 */
+	public static AsnElt EncodePublicKey(IPublicKey pk)
+	{
+		RSAPublicKey rk = pk as RSAPublicKey;
+		/* disabled DSA
+		DSAPublicKey dk = pk as DSAPublicKey;
+		*/
+		ECPublicKey ek = pk as ECPublicKey;
+		string oid;
+		AsnElt app;
+		byte[] pkv;
+		if (rk != null) {
+			oid = OID_RSA;
+			app = AsnElt.NULL_V;
+			pkv = AsnElt.Make(AsnElt.SEQUENCE,
+				AsnElt.MakeInteger(rk.Modulus),
+				AsnElt.MakeInteger(rk.Exponent)).Encode();
+		/* disabled DSA
+		} else if (dk != null) {
+			oid = OID_DSA;
+			app = AsnElt.Make(AsnElt.SEQUENCE,
+				AsnElt.MakeInteger(dk.P),
+				AsnElt.MakeInteger(dk.Q),
+				AsnElt.MakeInteger(dk.G));
+			pkv = AsnElt.MakeInteger(dk.Y).Encode();
+		*/
+		} else if (ek != null) {
+			oid = OID_EC;
+			app = AsnElt.MakeOID(CurveToOID(ek.Curve));
+			pkv = ek.Pub;
+		} else {
+			throw new ArgumentException(
+				"Cannot encode key type: " + pk.AlgorithmName);
+		}
+		AsnElt ai;
+		if (app == null) {
+			ai = AsnElt.Make(AsnElt.SEQUENCE,
+				AsnElt.MakeOID(oid));
+		} else {
+			ai = AsnElt.Make(AsnElt.SEQUENCE,
+				AsnElt.MakeOID(oid),
+				app);
+		}
+		return AsnElt.Make(AsnElt.SEQUENCE,
+			ai,
+			AsnElt.MakeBitString(pkv));
+	}
+
+	/*
+	 * Decode a public key (SubjectPublicKeyInfo).
+	 */
+	public static IPublicKey DecodePublicKey(byte[] spki)
+	{
+		string pemType = null;
+		spki = AsnIO.FindBER(spki, false, out pemType);
+		if (spki == null) {
+			throw new AsnException("Not an encoded object");
+		}
+		return DecodePublicKey(AsnElt.Decode(spki));
+	}
+
+	/*
+	 * Decode a public key (SubjectPublicKeyInfo).
+	 */
+	public static IPublicKey DecodePublicKey(AsnElt ak)
+	{
+		ak.CheckNumSub(2);
+		AlgorithmIdentifier ai = new AlgorithmIdentifier(ak.GetSub(0));
+		AsnElt abs = ak.GetSub(1);
+		abs.CheckTag(AsnElt.BIT_STRING);
+		byte[] pub = abs.GetBitString();
+		switch (ai.OID) {
+		case OID_RSA:
+		case OID_RSA_OAEP:
+		case OID_RSA_PSS:
+			return DecodePublicKeyRSA(pub);
+		/* disabled DSA
+		case OID_DSA:
+			return DecodePublicKeyDSA(pub);
+		*/
+		case OID_EC:
+			/*
+			 * For elliptic curves, the parameters should
+			 * include the curve specification.
+			 */
+			AsnElt ap = ai.Parameters;
+			if (ap == null) {
+				throw new AsnException("No curve specified"
+					+ " for EC public key");
+			}
+			if (ap.TagClass != AsnElt.UNIVERSAL
+				|| ap.TagValue != AsnElt.OBJECT_IDENTIFIER)
+			{
+				throw new AsnException("Unsupported type"
+					+ " of curve specification");
+			}
+			return new ECPublicKey(OIDToCurve(ap.GetOID()), pub);
+		default:
+			throw new AsnException(
+				"Unknown public key type: " + ai.OID);
+		}
+	}
+
+	static IPublicKey DecodePublicKeyRSA(byte[] pub)
+	{
+		AsnElt ae = AsnElt.Decode(pub);
+		ae.CheckTag(AsnElt.SEQUENCE);
+		ae.CheckNumSub(2);
+		byte[] n = GetPositiveInteger(ae.GetSub(0));
+		byte[] e = GetPositiveInteger(ae.GetSub(1));
+		return new RSAPublicKey(n, e);
+	}
+
+	static string CurveToOID(ECCurve curve)
+	{
+		switch (curve.Name) {
+		case "P-192":
+			return "1.2.840.10045.3.1.1";
+		case "P-224":
+			return "1.3.132.0.33";
+		case "P-256":
+			return "1.2.840.10045.3.1.7";
+		case "P-384":
+			return "1.3.132.0.34";
+		case "P-521":
+			return "1.3.132.0.35";
+		}
+		throw new ArgumentException(string.Format(
+			"No known OID for curve '{0}'", curve.Name));
+	}
+
+	static ECCurve OIDToCurve(string oid)
+	{
+		switch (oid) {
+		/*
+		case "1.2.840.10045.3.1.1":
+			return NIST.P192;
+		case "1.3.132.0.33":
+			return NIST.P224;
+		*/
+		case "1.2.840.10045.3.1.7":
+			return NIST.P256;
+		case "1.3.132.0.34":
+			return NIST.P384;
+		case "1.3.132.0.35":
+			return NIST.P521;
+		}
+		throw new ArgumentException(string.Format(
+			"No known curve for OID {0}", oid));
+	}
+
+	static byte[] GetPositiveInteger(AsnElt ae)
+	{
+		ae.CheckTag(AsnElt.INTEGER);
+		byte[] x = ae.CopyValue();
+		if (x.Length == 0) {
+			throw new AsnException("Invalid integer (empty)");
+		}
+		if (x[0] >= 0x80) {
+			throw new AsnException("Invalid integer (negative)");
+		}
+		return x;
+	}
+
+	static int Dec16be(byte[] buf, int off)
+	{
+		return (buf[off] << 8) + buf[off + 1];
+	}
+
+	static int Dec24be(byte[] buf, int off)
+	{
+		return (buf[off] << 16) + (buf[off + 1] << 8) + buf[off + 2];
+	}
+
+	const string B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+		+ "abcdefghijklmnopqrstuvwxyz0123456789+/";
+
+	public static string ToBase64(byte[] buf, int off, int len)
+	{
+		char[] tc = new char[((len + 2) / 3) << 2];
+		for (int i = 0, j = 0; i < len; i += 3) {
+			if ((i + 3) <= len) {
+				int x = Dec24be(buf, off + i);
+				tc[j ++] = B64[x >> 18];
+				tc[j ++] = B64[(x >> 12) & 0x3F];
+				tc[j ++] = B64[(x >> 6) & 0x3F];
+				tc[j ++] = B64[x & 0x3F];
+			} else if ((i + 2) == len) {
+				int x = Dec16be(buf, off + i);
+				tc[j ++] = B64[x >> 10];
+				tc[j ++] = B64[(x >> 4) & 0x3F];
+				tc[j ++] = B64[(x << 2) & 0x3F];
+				tc[j ++] = '=';
+			} else if ((i + 1) == len) {
+				int x = buf[off + i];
+				tc[j ++] = B64[(x >> 2) & 0x3F];
+				tc[j ++] = B64[(x << 4) & 0x3F];
+				tc[j ++] = '=';
+				tc[j ++] = '=';
+			}
+		}
+		return new string(tc);
+	}
+
+	public static void WritePEM(TextWriter w, string objType, byte[] buf)
+	{
+		w.WriteLine("-----BEGIN {0}-----", objType.ToUpperInvariant());
+		int n = buf.Length;
+		for (int i = 0; i < n; i += 57) {
+			int len = Math.Min(57, n - i);
+			w.WriteLine(ToBase64(buf, i, len));
+		}
+		w.WriteLine("-----END {0}-----", objType.ToUpperInvariant());
+	}
+
+	public static string ToPEM(string objType, byte[] buf)
+	{
+		return ToPEM(objType, buf, "\n");
+	}
+
+	public static string ToPEM(string objType, byte[] buf, string nl)
+	{
+		StringWriter w = new StringWriter();
+		w.NewLine = nl;
+		WritePEM(w, objType, buf);
+		return w.ToString();
+	}
+}
+
+}
diff --git a/ZInt/ZInt.cs b/ZInt/ZInt.cs
new file mode 100644
index 0000000..c8333f4
--- /dev/null
+++ b/ZInt/ZInt.cs
@@ -0,0 +1,3250 @@
+/*
+ * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining 
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be 
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
+ * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
+ * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+ * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+using System;
+using System.Security.Cryptography;
+using System.Text;
+
+/*
+ * A custom "big integer" implementation. It internally uses an array
+ * of 32-bit integers, that encode the integer in little-endian convention,
+ * using two's complement for negative integers.
+ *
+ * Apart from the array, a single 32-bit field is also present, which
+ * encodes the sign. When the value is small (it fits on 32 bits), then
+ * the array pointer is null, and the value is in the 32-bit field.
+ * Since ZInt is a struct, this means that computations using ZInt do
+ * not entail any dynamic (GC-based) memory allocation as long as the
+ * value fits on 32 bits. This makes it substantially faster than usual
+ * "big integer" implementations (including .NET's implementation, since
+ * version 4.0) when values are small.
+ *
+ * Instances are immutable, and thus can be used as if they were
+ * "plain integers".
+ *
+ * None of this code is "constant-time". As such, ZInt should be
+ * considered unsuitable to implementations of cryptographic algorithms.
+ */
+
+public struct ZInt : IComparable, IComparable<ZInt>, IEquatable<ZInt> {
+
+	/*
+	 * CONVENTIONS:
+	 *
+	 * If varray == null, then "small" contains the integer value.
+	 *
+	 * If varray != null, then it contains the value, in little-endian
+	 * convention (least significant word comes first) and of
+	 * minimal encoded length (i.e. "trimmed"). Two's complement is
+	 * used for negative values. "small" is then -1 or 0, depending
+	 * on whether the value is negative or not.
+	 *
+	 * Note that the trimmed value does not always include the sign
+	 * bit.
+	 *
+	 * If the integer value is in the range of values which can be
+	 * represented in an "int", then magn is null. There is no allowed
+	 * overlap between the two kinds of encodings.
+	 *
+	 * Default value thus encodes the integer zero.
+	 */
+
+	readonly int small;
+	readonly uint[] varray;
+
+	/*
+	 * The value -1.
+	 */
+	public static ZInt MinusOne {
+		get { return new ZInt(-1, null); }
+	}
+
+	/*
+	 * The value 0.
+	 */
+	public static ZInt Zero {
+		get { return new ZInt(0, null); }
+	}
+
+	/*
+	 * The value 1.
+	 */
+	public static ZInt One {
+		get { return new ZInt(1, null); }
+	}
+
+	/*
+	 * The value 2.
+	 */
+	public static ZInt Two {
+		get { return new ZInt(2, null); }
+	}
+
+	/*
+	 * Internal constructor which assumes that the provided values are
+	 * correct and normalized.
+	 */
+	private ZInt(int small, uint[] varray)
+	{
+		this.small = small;
+		this.varray = varray;
+#if DEBUG
+		if (varray != null) {
+			if (small != -1 && small != 0) {
+				throw new Exception(
+					"Bad sign encoding: " + small);
+			}
+			if (varray.Length == 0) {
+				throw new Exception("Empty varray");
+			}
+			if (Length(small, varray) != varray.Length) {
+				throw new Exception("Untrimmed varray");
+			}
+			if (varray.Length == 1) {
+				/*
+				 * If there was room for a sign bit, then
+				 * the "small" encoding should have been used.
+				 */
+				if (((varray[0] ^ (uint)small) >> 31) == 0) {
+					throw new Exception(
+						"suboptimal encoding");
+				}
+			}
+		}
+#endif
+	}
+
+	/*
+	 * Main internal build method. This method normalizes the encoding
+	 * ("small" encoding is used when possible; otherwise, the varray
+	 * is trimmed and the sign is normalized to -1 or 0).
+	 *
+	 * If "varray" is null, then the small value is used. Otherwise,
+	 * only the sign bit (most significant bit) of "small" is used,
+	 * and that value is normalized; the array is trimmed. If the
+	 * small encoding then becomes applicable, then it is used.
+	 */
+	private static ZInt Make(int small, uint[] varray)
+	{
+		if (varray == null) {
+			return new ZInt(small, null);
+		}
+		small >>= 31;
+		int n = Length(small, varray);
+		if (n == 1 && (((varray[0] ^ (uint)small) >> 31) == 0)) {
+			small = (int)varray[0];
+			varray = null;
+		} else {
+			/*
+			 * Note: if n == 0 then the value is -1 or 0, and
+			 * "small" already contains the correct value;
+			 * Trim() will then return null, which is appropriate.
+			 */
+			varray = Trim(small, varray, n);
+		}
+		return new ZInt(small, varray);
+	}
+
+	/*
+	 * Create an instance from a signed 32-bit integer.
+	 */
+	public ZInt(int val)
+	{
+		small = val;
+		varray = null;
+	}
+
+	/*
+	 * Create an instance from an unsigned 32-bit integer.
+	 */
+	public ZInt(uint val)
+	{
+		small = (int)val;
+		if (small < 0) {
+			small = 0;
+			varray = new uint[1] { val };
+		} else {
+			varray = null;
+		}
+	}
+
+	/*
+	 * Create an instance from a signed 64-bit integer.
+	 */
+	public ZInt(long val)
+	{
+		small = (int)val;
+		if ((long)small == val) {
+			varray = null;
+		} else {
+			ulong uval = (ulong)val;
+			uint w0 = (uint)uval;
+			uint w1 = (uint)(uval >> 32);
+			if (w1 == 0) {
+				small = 0;
+				varray = new uint[1] { w0 };
+			} else if (w1 == 0xFFFFFFFF) {
+				small = -1;
+				varray = new uint[1] { w0 };
+			} else {
+				small = (int)w1 >> 31;
+				varray = new uint[2] { w0, w1 };
+			}
+		}
+	}
+
+	/*
+	 * Create an instance from an unsigned 64-bit integer.
+	 */
+	public ZInt(ulong val)
+	{
+		if (val <= 0x7FFFFFFF) {
+			small = (int)val;
+			varray = null;
+		} else {
+			small = 0;
+			uint w0 = (uint)val;
+			uint w1 = (uint)(val >> 32);
+			if (w1 == 0) {
+				varray = new uint[1] { w0 };
+			} else {
+				varray = new uint[2] { w0, w1 };
+			}
+		}
+	}
+
+	/*
+	 * Create a ZInt instance for an integer expressed as an array
+	 * of 32-bit integers (unsigned little-endian convention), with
+	 * a specific number of value bits.
+	 */
+	public static ZInt Make(uint[] words, int numBits)
+	{
+		if (numBits == 0) {
+			return Zero;
+		}
+		int n = (numBits + 31) >> 5;
+		int kb = numBits & 31;
+		uint[] m = new uint[n];
+		Array.Copy(words, 0, m, 0, n);
+		if (kb > 0) {
+			m[n - 1] &= ~((uint)0xFFFFFFFF << kb);
+		}
+		return Make(0, m);
+	}
+
+	/*
+	 * Create a ZInt instance for an integer expressed as an array
+	 * of 64-bit integers (unsigned little-endian convention), with
+	 * a specific number of value bits.
+	 */
+	public static ZInt Make(ulong[] words, int numBits)
+	{
+		if (numBits == 0) {
+			return Zero;
+		}
+		int kw = (numBits + 63) >> 6;
+		int kb = numBits & 63;
+		int n = kw * 2;
+		uint[] m = new uint[n];
+		if (kb != 0) {
+			ulong z = words[kw - 1]
+				& ~((ulong)0xFFFFFFFFFFFFFFFF << kb);
+			m[n - 1] = (uint)(z >> 32);
+			m[n - 2] = (uint)z;
+			kw --;
+			n -= 2;
+		}
+		for (int i = kw - 1, j = n - 2; i >= 0; i --, j -= 2) {
+			ulong z = words[i];
+			m[j + 0] = (uint)z;
+			m[j + 1] = (uint)(z >> 32);
+		}
+		return Make(0, m);
+	}
+
+	/*
+	 * Test whether this value is 0.
+	 */
+	public bool IsZero {
+		get {
+			return small == 0 && varray == null;
+		}
+	}
+
+	/*
+	 * Test whether this value is 1.
+	 */
+	public bool IsOne {
+		get {
+			return small == 1;
+		}
+	}
+
+	/*
+	 * Test whether this value is even.
+	 */
+	public bool IsEven {
+		get {
+			uint w = (varray == null) ? (uint)small : varray[0];
+			return (w & 1) == 0;
+		}
+	}
+
+	/*
+	 * Test whether this value is a power of 2. Note that 1 is a power
+	 * of 2 (but 0 is not). For negative values, false is returned.
+	 */
+	public bool IsPowerOfTwo {
+		get {
+			if (small < 0) {
+				return false;
+			}
+			if (varray == null) {
+				return small != 0 && (small & -small) == small;
+			}
+			int n = varray.Length;
+			int z = (int)varray[n - 1];
+			if ((z & -z) != z) {
+				return false;
+			}
+			for (int i = n - 2; i >= 0; i --) {
+				if (varray[i] != 0) {
+					return false;
+				}
+			}
+			return true;
+		}
+	}
+
+	/*
+	 * Get the sign of this value as an integer (-1 for negative
+	 * values, 1 for positive, 0 for zero).
+	 */
+	public int Sign {
+		get {
+			if (varray == null) {
+				if (small < 0) {
+					return -1;
+				} else if (small == 0) {
+					return 0;
+				} else {
+					return 1;
+				}
+			} else {
+				return small | 1;
+			}
+		}
+	}
+
+	/*
+	 * Test whether this value would fit in an 'int'.
+	 */
+	public bool IsInt {
+		get {
+			return varray == null;
+		}
+	}
+
+	/*
+	 * Test whether this value would fit in a "long".
+	 */
+	public bool IsLong {
+		get {
+			if (varray == null) {
+				return true;
+			}
+			int n = varray.Length;
+			if (n == 1) {
+				return true;
+			} else if (n == 2) {
+				return ((int)varray[1] >> 31) == small;
+			} else {
+				return false;
+			}
+		}
+	}
+
+	/*
+	 * Get the length of the value in bits. This is the minimal number
+	 * of bits of the two's complement representation of the value,
+	 * excluding the sign bit; thus, both 0 and -1 have bit length 0.
+	 */
+	public int BitLength {
+		get {
+			if (varray == null) {
+				if (small < 0) {
+					return 32 - LeadingZeros(~(uint)small);
+				} else {
+					return 32 - LeadingZeros((uint)small);
+				}
+			} else {
+				int n = varray.Length;
+				int bl = n << 5;
+				uint w = varray[n - 1];
+				if (small < 0) {
+					return bl - LeadingZeros(~w);
+				} else {
+					return bl - LeadingZeros(w);
+				}
+			}
+		}
+	}
+
+	/*
+	 * Test whether the specified bit has value 1 or 0 ("true" is
+	 * returned if the bit has value 1). Note that for negative values,
+	 * two's complement representation is assumed.
+	 */
+	public bool TestBit(int n)
+	{
+		if (n < 0) {
+			throw new ArgumentOutOfRangeException();
+		}
+		if (varray == null) {
+			if (n >= 32) {
+				return small < 0;
+			} else {
+				return (((uint)small >> n) & (uint)1) != 0;
+			}
+		} else {
+			int nw = n >> 5;
+			if (nw >= varray.Length) {
+				return small != 0;
+			}
+			int nb = n & 31;
+			return ((varray[nw] >> nb) & (uint)1) != 0;
+		}
+	}
+
+	/*
+	 * Copy some bits from this instance to the provided array. Bits
+	 * are copied in little-endian order. First bit to be copied is
+	 * bit at index "off", and exactly "num" bits are copied. This
+	 * method modifies only the minimum number of destination words
+	 * (i.e. the first "(num+31)/32" words, exactly). Remaining bits
+	 * in the last touched word are set to 0.
+	 */
+	public void CopyBits(int off, int num, uint[] dest)
+	{
+		CopyBits(off, num, dest, 0);
+	}
+
+	public void CopyBits(int off, int num, uint[] dest, int destOff)
+	{
+		if (off < 0 || num < 0) {
+			throw new ArgumentOutOfRangeException();
+		}
+		if (num == 0) {
+			return;
+		}
+		ZInt x = this;
+		if (off > 0) {
+			x >>= off;
+		}
+		int kw = num >> 5;
+		int kb = num & 31;
+		uint hmask = ~((uint)0xFFFFFFFF << kb);
+		if (x.varray == null) {
+			if (kw == 0) {
+				dest[destOff] = (uint)x.small & hmask;
+			} else {
+				uint iw = (uint)(x.small >> 31);
+				dest[destOff] = (uint)x.small;
+				for (int i = 1; i < kw; i ++) {
+					dest[destOff + i] = iw;
+				}
+				if (kb > 0) {
+					dest[destOff + kw] = iw & hmask;
+				}
+			}
+		} else {
+			int n = x.varray.Length;
+			if (kw <= n) {
+				Array.Copy(x.varray, 0, dest, destOff, kw);
+			} else {
+				Array.Copy(x.varray, 0, dest, destOff, n);
+				for (int i = n; i < kw; i ++) {
+					dest[destOff + i] = (uint)x.small;
+				}
+			}
+			if (kb > 0) {
+				uint last;
+				if (kw < n) {
+					last = x.varray[kw] & hmask;
+				} else {
+					last = (uint)x.small & hmask;
+				}
+				dest[destOff + kw] = last;
+			}
+		}
+	}
+
+	/*
+	 * Copy some bits from this instance to the provided array. Bits
+	 * are copied in little-endian order. First bit to be copied is
+	 * bit at index "off", and exactly "num" bits are copied. This
+	 * method modifies only the minimum number of destination words
+	 * (i.e. the first "(num+63)/64" words, exactly). Remaining bits
+	 * in the last touched word are set to 0.
+	 */
+	public void CopyBits(int off, int num, ulong[] dest)
+	{
+		CopyBits(off, num, dest, 0);
+	}
+
+	public void CopyBits(int off, int num, ulong[] dest, int destOff)
+	{
+		if (off < 0 || num < 0) {
+			throw new ArgumentOutOfRangeException();
+		}
+		if (num == 0) {
+			return;
+		}
+		ZInt x = this;
+		if (off > 0) {
+			x >>= off;
+		}
+		int kw = num >> 6;
+		int kb = num & 63;
+		ulong hmask = ~((ulong)0xFFFFFFFFFFFFFFFF << kb);
+		long xs = (long)x.small;
+		if (x.varray == null) {
+			if (kw == 0) {
+				dest[destOff] = (ulong)xs & hmask;
+			} else {
+				ulong iw = (ulong)(xs >> 31);
+				dest[destOff] = (ulong)xs;
+				for (int i = 1; i < kw; i ++) {
+					dest[destOff + i] = iw;
+				}
+				if (kb > 0) {
+					dest[destOff + kw] = iw & hmask;
+				}
+			}
+		} else {
+			int n = x.varray.Length;
+			uint iw = (uint)x.small;
+			int j = 0;
+			for (int i = 0; i < kw; i ++, j += 2) {
+				uint w0 = (j < n) ? x.varray[j] : iw;
+				uint w1 = ((j + 1) < n) ? x.varray[j + 1] : iw;
+				dest[destOff + i] =
+					(ulong)w0 | ((ulong)w1 << 32);
+			}
+			if (kb > 0) {
+				uint w0 = (j < n) ? x.varray[j] : iw;
+				uint w1 = ((j + 1) < n) ? x.varray[j + 1] : iw;
+				ulong last = (ulong)w0 | ((ulong)w1 << 32);
+				dest[destOff + kw] = last & hmask;
+			}
+		}
+	}
+
+	/*
+	 * Extract a 32-bit word at a given offset (counted in bits).
+	 * This function is equivalent to right-shifting the value by
+	 * "off" bits, then returning the low 32 bits (however, this
+	 * function may be more efficient).
+	 */
+	public uint GetWord(int off)
+	{
+		if (off < 0) {
+			throw new ArgumentOutOfRangeException();
+		}
+		if (varray == null) {
+			int x = small;
+			if (off >= 32) {
+				off = 31;
+			}
+			return (uint)(x >> off);
+		}
+		int n = varray.Length;
+		int kw = off >> 5;
+		if (kw >= n) {
+			return (uint)small;
+		}
+		int kb = off & 31;
+		if (kb == 0) {
+			return varray[kw];
+		} else {
+			uint hi;
+			if (kw == n - 1) {
+				hi = (uint)small;
+			} else {
+				hi = varray[kw + 1];
+			}
+			uint lo = varray[kw];
+			return (lo >> kb) | (hi << (32 - kb));
+		}
+	}
+
+	/*
+	 * Extract a 64-bit word at a given offset (counted in bits).
+	 * This function is equivalent to right-shifting the value by
+	 * "off" bits, then returning the low 64 bits (however, this
+	 * function may be more efficient).
+	 */
+	public ulong GetWord64(int off)
+	{
+		if (off < 0) {
+			throw new ArgumentOutOfRangeException();
+		}
+		if (varray == null) {
+			int x = small;
+			if (off >= 32) {
+				off = 31;
+			}
+			return (ulong)(x >> off);
+		}
+		int n = varray.Length;
+		int kw = off >> 5;
+		if (kw >= n) {
+			return (ulong)small;
+		}
+		int kb = off & 31;
+		if (kb == 0) {
+			if (kw == (n - 1)) {
+				return (ulong)varray[kw]
+					| ((ulong)small << 32);
+			} else {
+				return (ulong)varray[kw]
+					| ((ulong)varray[kw + 1] << 32);
+			}
+		} else {
+			uint v0, v1, v2;
+			if (kw == (n - 1)) {
+				v0 = varray[kw];
+				v1 = (uint)small;
+				v2 = (uint)small;
+			} else if (kw == (n - 2)) {
+				v0 = varray[kw];
+				v1 = varray[kw + 1];
+				v2 = (uint)small;
+			} else {
+				v0 = varray[kw];
+				v1 = varray[kw + 1];
+				v2 = varray[kw + 2];
+			}
+			uint lo = (v0 >> kb) | (v1 << (32 - kb));
+			uint hi = (v1 >> kb) | (v2 << (32 - kb));
+			return (ulong)lo | ((ulong)hi << 32);
+		}
+	}
+
+	/*
+	 * Convert this value to an 'int', using silent truncation if
+	 * the value does not fit.
+	 */
+	public int ToInt {
+		get {
+			return (varray == null) ? small : (int)varray[0];
+		}
+	}
+
+	/*
+	 * Convert this value to an 'uint', using silent truncation if
+	 * the value does not fit.
+	 */
+	public uint ToUInt {
+		get {
+			return (varray == null) ? (uint)small : varray[0];
+		}
+	}
+
+	/*
+	 * Convert this value to a 'long', using silent truncation if
+	 * the value does not fit.
+	 */
+	public long ToLong {
+		get {
+			return (long)ToULong;
+		}
+	}
+
+	/*
+	 * Convert this value to an 'ulong', using silent truncation if
+	 * the value does not fit.
+	 */
+	public ulong ToULong {
+		get {
+			if (varray == null) {
+				return (ulong)small;
+			} else if (varray.Length == 1) {
+				uint iw = (uint)small;
+				return (ulong)varray[0] | ((ulong)iw << 32);
+			} else {
+				return (ulong)varray[0]
+					| ((ulong)varray[1] << 32);
+			}
+		}
+	}
+
+	/*
+	 * Get the actual length of a varray encoding: this is the minimal
+	 * length, in words, needed to encode the value. The value sign
+	 * is provided as a negative or non-negative integer, and the
+	 * encoding of minimal length does not necessarily include a
+	 * sign bit. The value 0 is returned when the array encodes 0
+	 * or -1 (depending on sign).
+	 */
+	static int Length(int sign, uint[] m)
+	{
+		if (m == null) {
+			return 0;
+		}
+		uint sw = (uint)(sign >> 31);
+		int n = m.Length;
+		while (n > 0 && m[n - 1] == sw) {
+			n --;
+		}
+		return n;
+	}
+
+	/*
+	 * Trim an encoding to its minimal encoded length. If the provided
+	 * array is already of minimal length, it is returned unchanged.
+	 */
+	static uint[] Trim(int sign, uint[] m)
+	{
+		int n = Length(sign, m);
+		if (n == 0) {
+			return null;
+		} else if (n < m.Length) {
+			uint[] mt = new uint[n];
+			Array.Copy(m, 0, mt, 0, n);
+			return mt;
+		} else {
+			return m;
+		}
+	}
+
+	/*
+	 * Trim or extend a value to the provided length. The returned
+	 * array will have the length specified as "n" (if n == 0, then
+	 * null is returned). If the source array already has the right
+	 * size, then it is returned unchanged.
+	 */
+	static uint[] Trim(int sign, uint[] m, int n)
+	{
+		if (n == 0) {
+			return null;
+		} else if (m == null) {
+			m = new uint[n];
+			if (sign < 0) {
+				Fill(0xFFFFFFFF, m);
+			}
+			return m;
+		}
+		int ct = m.Length;
+		if (ct < n) {
+			uint[] r = new uint[n];
+			Array.Copy(m, 0, r, 0, ct);
+			return r;
+		} else if (ct == n) {
+			return m;
+		} else {
+			uint[] r = new uint[n];
+			Array.Copy(m, 0, r, 0, n);
+			if (sign < 0) {
+				Fill(0xFFFFFFFF, r, ct, n - ct);
+			}
+			return r;
+		}
+	}
+
+	static void Fill(uint val, uint[] buf)
+	{
+		Fill(val, buf, 0, buf.Length);
+	}
+
+	static void Fill(uint val, uint[] buf, int off, int len)
+	{
+		while (len -- > 0) {
+			buf[off ++] = val;
+		}
+	}
+
+	// =================================================================
+	/*
+	 * Utility methods.
+	 *
+	 * The methods whose name begins with "Mutate" modify the array
+	 * they are given as first parameter; the other methods instantiate
+	 * a new array.
+	 *
+	 * As a rule, untrimmed arrays are accepted as input, and output
+	 * may be untrimmed as well.
+	 */
+
+	/*
+	 * Count the number of leading zeros for a 32-bit value (number of
+	 * consecutive zeros, starting with the most significant bit). This
+	 * value is between 0 (for a value equal to 2^31 or greater) and
+	 * 32 (for zero).
+	 */
+	static int LeadingZeros(uint v)
+	{
+		if (v == 0) {
+			return 32;
+		}
+		int n = 0;
+		if (v > 0xFFFF) { v >>= 16; } else { n += 16; }
+		if (v > 0x00FF) { v >>=  8; } else { n +=  8; }
+		if (v > 0x000F) { v >>=  4; } else { n +=  4; }
+		if (v > 0x0003) { v >>=  2; } else { n +=  2; }
+		if (v <= 0x0001) { n ++; }
+		return n;
+	}
+
+	/*
+	 * Duplicate the provided magnitude array. No attempt is made at
+	 * trimming. The source array MUST NOT be null.
+	 */
+	static uint[] Dup(uint[] m)
+	{
+		uint[] r = new uint[m.Length];
+		Array.Copy(m, 0, r, 0, m.Length);
+		return r;
+	}
+
+	/*
+	 * Increment the provided array. If there is a resulting carry,
+	 * then "true" is returned, "false" otherwise. The array MUST
+	 * NOT be null.
+	 */
+	static bool MutateIncr(uint[] x)
+	{
+		int n = x.Length;
+		for (int i = 0; i < n; i ++) {
+			uint w = x[i] + 1;
+			x[i] = w;
+			if (w != 0) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	/*
+	 * Decrement the provided array. If there is a resulting carry,
+	 * then "true" is returned, "false" otherwise. The array MUST
+	 * NOT be null.
+	 */
+	static bool MutateDecr(uint[] x)
+	{
+		int n = x.Length;
+		for (int i = 0; i < n; i ++) {
+			uint w = x[i];
+			x[i] = w - 1;
+			if (w != 0) {
+				return false;
+			}
+		}
+		return true;
+	}
+
+	/*
+	 * Multiply a[] with b[] (unsigned multiplication).
+	 */
+	static uint[] Mul(uint[] a, uint[] b)
+	{
+		// TODO: use Karatsuba when operands are large.
+		int na = Length(0, a);
+		int nb = Length(0, b);
+		if (na == 0 || nb == 0) {
+			return null;
+		}
+		uint[] r = new uint[na + nb];
+		for (int i = 0; i < na; i ++) {
+			ulong ma = a[i];
+			ulong carry = 0;
+			for (int j = 0; j < nb; j ++) {
+				ulong mb = (ulong)b[j];
+				ulong mr = (ulong)r[i + j];
+				ulong w = ma * mb + mr + carry;
+				r[i + j] = (uint)w;
+				carry = w >> 32;
+			}
+			r[i + nb] = (uint)carry;
+		}
+		return r;
+	}
+
+	/*
+	 * Get the sign and magnitude of an integer. The sign is
+	 * normalized to -1 (negative) or 0 (positive or zero). The
+	 * magnitude is an array of length at least 1, containing the
+	 * absolute value of this integer; if possible, the varray
+	 * is reused (hence, the magnitude array MUST NOT be altered).
+	 */
+	static void ToAbs(ZInt x, out int sign, out uint[] magn)
+	{
+		if (x.small < 0) {
+			sign = -1;
+			x = -x;
+		} else {
+			sign = 0;
+		}
+		magn = x.varray;
+		if (magn == null) {
+			magn = new uint[1] { (uint)x.small }; 
+		}
+	}
+
+	/*
+	 * Compare two integers, yielding -1, 0 or 1.
+	 */
+	static int Compare(int a, int b)
+	{
+		if (a < b) {
+			return -1;
+		} else if (a == b) {
+			return 0;
+		} else {
+			return 1;
+		}
+	}
+
+	/*
+	 * Compare a[] with b[] (unsigned). Returned value is 1 if a[]
+	 * is greater than b[], 0 if they are equal, -1 otherwise.
+	 */
+	static int Compare(uint[] a, uint[] b)
+	{
+		int ka = Length(0, a);
+		int kb = Length(0, b);
+		if (ka < kb) {
+			return -1;
+		} else if (ka == kb) {
+			while (ka > 0) {
+				ka --;
+				uint wa = a[ka];
+				uint wb = b[ka];
+				if (wa < wb) {
+					return -1;
+				} else if (wa > wb) {
+					return 1;
+				}
+			}
+			return 0;
+		} else {
+			return 1;
+		}
+	}
+
+	/*
+	 * Add b[] to a[] (unsigned). a[] is modified "in place". Only
+	 * n words of a[] are modified. Moreover, the value of
+	 * b[] which is added is left-shifted: words b[0]...b[n-1] are
+	 * added to a[k]...a[k+n-1]. The final carry is returned ("true"
+	 * for 1, "false" for 0). Neither a nor b may be null.
+	 */
+	static bool MutateAdd(uint[] a, int n, uint[] b, int k)
+	{
+		bool carry = false;
+		for (int i = 0; i < n; i ++) {
+			uint wa = a[i + k];
+			uint wb = b[i];
+			uint wc = wa + wb;
+			if (carry) {
+				wc ++;
+				carry = wa >= wc;
+			} else {
+				carry = wa > wc;
+			}
+			a[i + k] = wc;
+		}
+		return carry;
+	}
+
+	/*
+	 * Substract b[] from a[] (unsigned). a[] is modified "in
+	 * place". Only n words of a[] are modified. Words
+	 * b[0]...b[n-1] are subtracted from a[k]...a[k+n-1]. The final
+	 * carry is returned ("true" for -1, "false" for 0). Neither a
+	 * nor b may be null.
+	 */
+	static bool MutateSub(uint[] a, int n, uint[] b, int k)
+	{
+		bool carry = false;
+		for (int i = 0; i < n; i ++) {
+			uint wa = a[i + k];
+			uint wb = b[i];
+			uint wc = wa - wb;
+			if (carry) {
+				wc --;
+				carry = wa <= wc;
+			} else {
+				carry = wa < wc;
+			}
+			a[i + k] = wc;
+		}
+		return carry;
+	}
+
+	/*
+	 * Get the length (in words) of the result of a left-shift of
+	 * the provided integer by k bits. If k < 0, then the value is
+	 * computed for a right-shift by -k bits.
+	 */
+	static int GetLengthForLeftShift(ZInt x, int k)
+	{
+		if (k < 0) {
+			if (k == Int32.MinValue) {
+				return 0;
+			}
+			return GetLengthForRightShift(x, -k);
+		}
+		uint bl = (uint)x.BitLength + (uint)k;
+		return (int)((bl + 31) >> 5);
+	}
+
+	/*
+	 * Get the length (in words) of the result of a right-shift of
+	 * the provided integer by k bits. If k < 0, then the value is
+	 * computed for a left-shift by -k bits.
+	 */
+	static int GetLengthForRightShift(ZInt x, int k)
+	{
+		if (k < 0) {
+			if (k == Int32.MinValue) {
+				throw new OverflowException();
+			}
+			return GetLengthForLeftShift(x, -k);
+		}
+		uint bl = (uint)x.BitLength;
+		if (bl <= (uint)k) {
+			return 0;
+		} else {
+			return (int)((bl - k + 31) >> 5);
+		}
+	}
+
+	/*
+	 * Left-shift a[] (unsigned) by k bits. If k < 0, then this becomes
+	 * a right-shift.
+	 */
+	static uint[] ShiftLeft(uint[] a, int k)
+	{
+		if (k < 0) {
+			return ShiftRight(a, -k);
+		} else if (k == 0) {
+			return a;
+		}
+		int n = Length(0, a);
+		if (n == 0) {
+			return null;
+		}
+
+		/*
+		 * Allocate the result array, with the exact proper size.
+		 */
+		int bl = ((n << 5) - LeadingZeros(a[n - 1])) + k;
+		uint[] r = new uint[(bl + 31) >> 5];
+
+		int kb = k & 31;
+		int kw = k >> 5;
+
+		/*
+		 * Special case: shift by an integral amount of words.
+		 */
+		if (kb == 0) {
+			Array.Copy(a, 0, r, kw, n);
+			return r;
+		}
+
+		/*
+		 * Copy the bits. This loop handles one source word at
+		 * a time, and writes one destination word at a time.
+		 * Some unhandled bits may remain at the end.
+		 */
+		uint bits = 0;
+		int zkb = 32 - kb;
+		for (int i = 0; i < n; i ++) {
+			uint w = a[i];
+			r[i + kw] = bits | (w << kb);
+			bits = w >> zkb;
+		}
+		if (bits != 0) {
+			r[n + kw] = bits;
+		}
+		return r;
+	}
+
+	/*
+	 * Right-shift a[] by k bits. If k < 0, then this becomes
+	 * a left-shift.
+	 */
+	static uint[] ShiftRight(uint[] a, int k)
+	{
+		if (k < 0) {
+			return ShiftLeft(a, -k);
+		} else if (k == 0) {
+			return a;
+		}
+		int n = Length(0, a);
+		if (n == 0) {
+			return null;
+		}
+		int bl = (n << 5) - LeadingZeros(a[n - 1]) - k;
+		if (bl <= 0) {
+			return null;
+		}
+		uint[] r = new uint[(bl + 31) >> 5];
+
+		int kb = k & 31;
+		int kw = k >> 5;
+
+		/*
+		 * Special case: shift by an integral amount of words.
+		 */
+		if (kb == 0) {
+			Array.Copy(a, kw, r, 0, r.Length);
+			return r;
+		}
+
+		/*
+		 * Copy the bits. This loop handles one source word at
+		 * a time, and writes one destination word at a time.
+		 * Some unhandled bits may remain at the end.
+		 */
+		uint bits = a[kw] >> kb;
+		int zkb = 32 - kb;
+		for (int i = kw + 1; i < n; i ++) {
+			uint w = a[i];
+			r[i - kw - 1] = bits | (w << zkb);
+			bits = w >> kb;
+		}
+		if (bits != 0) {
+			r[n - kw - 1] = bits;
+		}
+		return r;
+	}
+
+	/*
+	 * Euclidian division of a[] (unsigned) by b (single word). This
+	 * method assumes that b is not 0.
+	 */
+	static void DivRem(uint[] a, uint b, out uint[] q, out uint r)
+	{
+		int n = Length(0, a);
+		if (n == 0) {
+			q = null;
+			r = 0;
+			return;
+		}
+		q = new uint[n];
+		ulong carry = 0;
+		for (int i = n - 1; i >= 0; i --) {
+			/*
+			 * Performance: we strongly hope that the JIT
+			 * compiler will notice that the "/" and "%"
+			 * can be combined into a single operation.
+			 * TODO: test whether we should replace the
+			 * carry computation with:
+			 *  carry = w - (ulong)q[i] * b;
+			 */
+			ulong w = (ulong)a[i] + (carry << 32);
+			q[i] = (uint)(w / b);
+			carry = w % b;
+		}
+		r = (uint)carry;
+	}
+
+	/*
+	 * Euclidian division of a[] (unsigned) by b (single word). This
+	 * method assumes that b is not 0. a[] is modified in place. The
+	 * remainder (in the 0..b-1 range) is returned.
+	 */
+	static uint MutateDivRem(uint[] a, uint b)
+	{
+		int n = Length(0, a);
+		if (n == 0) {
+			return 0;
+		}
+		ulong carry = 0;
+		for (int i = n - 1; i >= 0; i --) {
+			/*
+			 * Performance: we strongly hope that the JIT
+			 * compiler will notice that the "/" and "%"
+			 * can be combined into a single operation.
+			 * TODO: test whether we should replace the
+			 * carry computation with:
+			 *  carry = w - (ulong)q[i] * b;
+			 */
+			ulong w = (ulong)a[i] + (carry << 32);
+			a[i] = (uint)(w / b);
+			carry = w % b;
+		}
+		return (uint)carry;
+	}
+
+	/*
+	 * Euclidian division of a[] by b[] (unsigned). This method
+	 * assumes that b[] is neither 0 or 1, and a[] is not smaller
+	 * than b[] (this implies that the quotient won't be zero).
+	 */
+	static void DivRem(uint[] a, uint[] b, out uint[] q, out uint[] r)
+	{
+		int nb = Length(0, b);
+
+		/*
+		 * Special case when the divisor fits on one word.
+		 */
+		if (nb == 1) {
+			r = new uint[1];
+			DivRem(a, b[0], out q, out r[0]);
+			return;
+		}
+
+		/*
+		 * General case.
+		 *
+		 * We first normalize divisor and dividend such that the
+		 * most significant bit of the most significant word of
+		 * the divisor is set. We can then compute the quotient
+		 * word by word. In details:
+		 *
+		 * Let:
+		 *   w = 2^32 (one word)
+		 *   a = (w*a0 + a1) * w^N + a2
+		 *   b = b0 * w^N + b2
+		 * such that:
+		 *   0 <= a0 < w
+		 *   0 <= a1 < w
+		 *   0 <= a2 < w^N
+		 *   w/2 <= b0 < w
+		 *   0 <= b2 < w^N
+		 *   a < w*b
+		 * In other words, a0 and a1 are the two upper words of a[],
+		 * b0 is the upper word of b[] and has length 32 bits exactly,
+		 * and the quotient of a by b fits in one word.
+		 *
+		 * Under these conditions, define q and r such that:
+		 *   a = b * q + r
+		 *   q >= 0
+		 *   0 <= r < b
+		 * We can then compute a value u this way:
+		 *   if a0 = b0, then let u = w-1
+		 *   otherwise, let u be such that
+		 *     (w*a0 + a1) = u*b0 + v, where 0 <= v < b0
+		 * It can then be shown that all these inequations hold:
+		 *   0 <= u < w
+		 *   u-2 <= q <= u
+		 *
+		 * In plain words, this allows us to compute an almost-exact
+		 * estimate of the upper word of the quotient, with only
+		 * one 64-bit division.
+		 */
+
+		/*
+		 * Normalize dividend and divisor. The normalized dividend
+		 * will become the temporary array for the remainder, and
+		 * we will modify it, so we make sure we have a copy.
+		 */
+		int norm = LeadingZeros(b[nb - 1]);
+		r = ShiftLeft(a, norm);
+		if (r == a) {
+			r = new uint[a.Length];
+			Array.Copy(a, 0, r, 0, a.Length);
+		}
+		uint[] b2 = ShiftLeft(b, norm);
+		int nr = Length(0, r);
+#if DEBUG
+		if (Length(0, b2) != nb) {
+			throw new Exception("normalize error 1");
+		}
+		if (b2[nb - 1] < 0x80000000) {
+			throw new Exception("normalize error 2");
+		}
+		{
+			uint[] ta = ShiftRight(r, norm);
+			if (Compare(a, ta) != 0) {
+				throw new Exception("normalize error 3");
+			}
+			uint[] tb = ShiftRight(b2, norm);
+			if (Compare(b, tb) != 0) {
+				throw new Exception("normalize error 4");
+			}
+		}
+#endif
+		b = b2;
+
+		/*
+		 * Length of the quotient will be (at most) k words. This
+		 * is the number of iterations in the loop below.
+		 */
+		int k = (nr - nb) + 1;
+#if DEBUG
+		if (k <= 0) {
+			throw new Exception("wrong iteration count: " + k);
+		}
+#endif
+		q = new uint[k];
+
+		/*
+		 * The upper word of a[] (the one we modify, i.e. currently
+		 * stored in r[]) is in a0; it is carried over from the
+		 * previous loop iteration. Initially, it is zero.
+		 */
+		uint a0 = 0;
+		uint b0 = b[nb - 1];
+		int j = nr;
+		while (k -- > 0) {
+			uint a1 = r[-- j];
+			uint u;
+			if (a0 == b0) {
+				u = 0xFFFFFFFF;
+			} else {
+				ulong ah = ((ulong)a0 << 32) | (ulong)a1;
+				u = (uint)(ah / b0);
+			}
+
+			/*
+			 * Candidate word for the quotient:
+			 * -- if u = 0 then qw is necessarily 0, and the
+			 *    rest of this iteration is trivial;
+			 * -- if u = 1 then we try qw = 1;
+			 * -- otherwise, we try qw = u-1.
+			 */
+			uint qw;
+			if (u == 0) {
+				q[k] = 0;
+				a0 = a1;
+				continue;
+			} else if (u == 1) {
+				qw = 1;
+			} else {
+				qw = u - 1;
+			}
+
+			/*
+			 * "Trying" a candidate word means subtracting from
+			 * r[] the product qw*b (b[] being shifted by k words).
+			 * The result may be negative, in which case we
+			 * overestimated qw; it may be greater than b or
+			 * equal to b, in which case we underestimated qw;
+			 * or it may be just fine.
+			 */
+			ulong carry = 0;
+			bool tooBig = true;
+			for (int i = 0; i < nb; i ++) {
+				uint wb = b[i];
+				ulong z = (ulong)wb * (ulong)qw + carry;
+				carry = z >> 32;
+				uint wa = r[i + k];
+				uint wc = wa - (uint)z;
+				if (wc > wa) {
+					carry ++;
+				}
+				r[i + k] = wc;
+				if (wc != wb) {
+					tooBig = wc > wb;
+				}
+			}
+
+			/*
+			 * Once we have adjusted everything, the upper word
+			 * of r[] will be nullified; wo do it now. Note that
+			 * for the first loop iteration, that upper word
+			 * may be absent (so already zero, but also "virtual").
+			 */
+			if (nb + k < nr) {
+				r[nb + k] = 0;
+			}
+
+			/*
+			 * At that point, "carry" should be equal to a0
+			 * if we estimated right.
+			 */
+			if (carry < (ulong)a0) {
+				/*
+				 * Underestimate.
+				 */
+				qw ++;
+#if DEBUG
+				if (carry + 1 != (ulong)a0) {
+					throw new Exception("div error 1");
+				}
+				if (!MutateSub(r, nb, b, k)) {
+					throw new Exception("div error 2");
+				}
+#else
+				MutateSub(r, nb, b, k);
+#endif
+			} else if (carry > (ulong)a0) {
+				/*
+				 * Overestimate.
+				 */
+				qw --;
+#if DEBUG
+				if (carry - 1 != (ulong)a0) {
+					throw new Exception("div error 3");
+				}
+				if (!MutateAdd(r, nb, b, k)) {
+					throw new Exception("div error 4");
+				}
+#else
+				MutateAdd(r, nb, b, k);
+#endif
+			} else if (tooBig) {
+				/*
+				 * Underestimate, but no expected carry.
+				 */
+				qw ++;
+#if DEBUG
+				if (MutateSub(r, nb, b, k)) {
+					throw new Exception("div error 5");
+				}
+#else
+				MutateSub(r, nb, b, k);
+#endif
+			}
+
+			q[k] = qw;
+			a0 = r[j];
+		}
+
+		/*
+		 * At that point, r[] contains the remainder but needs
+		 * to be shifted back, to account for the normalization
+		 * performed before. q[] is correct (but possibly
+		 * untrimmed).
+		 */
+		r = ShiftRight(r, norm);
+	}
+
+	// =================================================================
+
+	/*
+	 * Conversion to sbyte; an OverflowException is thrown if the
+	 * value does not fit. Use ToInt to get a truncating conversion.
+	 */
+	public static explicit operator sbyte(ZInt val)
+	{
+		int x = (int)val;
+		if (x < SByte.MinValue || x > SByte.MaxValue) {
+			throw new OverflowException();
+		}
+		return (sbyte)x;
+	}
+
+	/*
+	 * Conversion to byte; an OverflowException is thrown if the
+	 * value does not fit. Use ToInt to get a truncating conversion.
+	 */
+	public static explicit operator byte(ZInt val)
+	{
+		int x = (int)val;
+		if (x > Byte.MaxValue) {
+			throw new OverflowException();
+		}
+		return (byte)x;
+	}
+
+	/*
+	 * Conversion to short; an OverflowException is thrown if the
+	 * value does not fit. Use ToInt to get a truncating conversion.
+	 */
+	public static explicit operator short(ZInt val)
+	{
+		int x = (int)val;
+		if (x < Int16.MinValue || x > Int16.MaxValue) {
+			throw new OverflowException();
+		}
+		return (short)x;
+	}
+
+	/*
+	 * Conversion to ushort; an OverflowException is thrown if the
+	 * value does not fit. Use ToInt to get a truncating conversion.
+	 */
+	public static explicit operator ushort(ZInt val)
+	{
+		int x = (int)val;
+		if (x > UInt16.MaxValue) {
+			throw new OverflowException();
+		}
+		return (ushort)x;
+	}
+
+	/*
+	 * Conversion to int; an OverflowException is thrown if the
+	 * value does not fit. Use ToInt to get a truncating conversion.
+	 */
+	public static explicit operator int(ZInt val)
+	{
+		if (val.varray != null) {
+			throw new OverflowException();
+		}
+		return val.small;
+	}
+
+	/*
+	 * Conversion to uint; an OverflowException is thrown if the
+	 * value does not fit. Use ToUInt to get a truncating conversion.
+	 */
+	public static explicit operator uint(ZInt val)
+	{
+		int s = val.small;
+		if (s < 0) {
+			throw new OverflowException();
+		}
+		uint[] m = val.varray;
+		if (m == null) {
+			return (uint)s;
+		} else if (m.Length > 1) {
+			throw new OverflowException();
+		} else {
+			return m[0];
+		}
+	}
+
+	/*
+	 * Conversion to long; an OverflowException is thrown if the
+	 * value does not fit. Use ToLong to get a truncating conversion.
+	 */
+	public static explicit operator long(ZInt val)
+	{
+		int s = val.small;
+		uint[] m = val.varray;
+		if (m == null) {
+			return (long)s;
+		} else if (m.Length == 1) {
+			return (long)m[0] | ((long)s << 32);
+		} else if (m.Length == 2) {
+			uint w0 = m[0];
+			uint w1 = m[1];
+			if (((w1 ^ (uint)s) >> 31) != 0) {
+				throw new OverflowException();
+			}
+			return (long)w0 | ((long)w1 << 32);
+		} else {
+			throw new OverflowException();
+		}
+	}
+
+	/*
+	 * Conversion to ulong; an OverflowException is thrown if the
+	 * value does not fit. Use ToULong to get a truncating conversion.
+	 */
+	public static explicit operator ulong(ZInt val)
+	{
+		int s = val.small;
+		if (s < 0) {
+			throw new OverflowException();
+		}
+		uint[] m = val.varray;
+		if (m == null) {
+			return (ulong)s;
+		} else if (m.Length == 1) {
+			return (ulong)m[0];
+		} else if (m.Length == 2) {
+			return (ulong)m[0] | ((ulong)m[1] << 32);
+		} else {
+			throw new OverflowException();
+		}
+	}
+
+	/*
+	 * By definition, conversion from sbyte conserves the value.
+	 */
+	public static implicit operator ZInt(sbyte val)
+	{
+		return new ZInt((int)val);
+	}
+
+	/*
+	 * By definition, conversion from byte conserves the value.
+	 */
+	public static implicit operator ZInt(byte val)
+	{
+		return new ZInt((uint)val);
+	}
+
+	/*
+	 * By definition, conversion from short conserves the value.
+	 */
+	public static implicit operator ZInt(short val)
+	{
+		return new ZInt((int)val);
+	}
+
+	/*
+	 * By definition, conversion from ushort conserves the value.
+	 */
+	public static implicit operator ZInt(ushort val)
+	{
+		return new ZInt((uint)val);
+	}
+
+	/*
+	 * By definition, conversion from int conserves the value.
+	 */
+	public static implicit operator ZInt(int val)
+	{
+		return new ZInt(val);
+	}
+
+	/*
+	 * By definition, conversion from uint conserves the value.
+	 */
+	public static implicit operator ZInt(uint val)
+	{
+		return new ZInt(val);
+	}
+
+	/*
+	 * By definition, conversion from long conserves the value.
+	 */
+	public static implicit operator ZInt(long val)
+	{
+		return new ZInt(val);
+	}
+
+	/*
+	 * By definition, conversion from ulong conserves the value.
+	 */
+	public static implicit operator ZInt(ulong val)
+	{
+		return new ZInt(val);
+	}
+
+	/*
+	 * Unary '+' operator is a no-operation.
+	 */
+	public static ZInt operator +(ZInt a)
+	{
+		return a;
+	}
+
+	/*
+	 * Unary negation.
+	 */
+	public static ZInt operator -(ZInt a)
+	{
+		int s = a.small;
+		uint[] m = a.varray;
+		if (m == null) {
+			if (s == Int32.MinValue) {
+				return new ZInt(0, new uint[1] { 0x80000000 });
+			} else {
+				return new ZInt(-s, null);
+			}
+		}
+
+		/*
+		 * Two's complement: invert all bits, then add 1. The
+		 * result array will usually have the same size, but may
+		 * be one word longer or one word shorter.
+		 */
+		int n = Length(s, m);
+		uint[] bm = new uint[n];
+		for (int i = 0; i < n; i ++) {
+			bm[i] = ~m[i];
+		}
+		if (MutateIncr(bm)) {
+			/*
+			 * Extra carry. This may happen only if the source
+			 * array contained only zeros, which may happen only
+			 * for a source value -2^(32*k) for some integer k
+			 * (k > 0).
+			 */
+			bm = new uint[n + 1];
+			bm[n] = 1;
+			return new ZInt(0, bm);
+		} else {
+			/*
+			 * The resulting array might be too big by one word,
+			 * so we must not assume that it is trimmed.
+			 */
+			return Make((int)~(uint)s, bm);
+		}
+	}
+
+	/*
+	 * Addition operator.
+	 */
+	public static ZInt operator +(ZInt a, ZInt b)
+	{
+		int sa = a.small;
+		int sb = b.small;
+		uint[] ma = a.varray;
+		uint[] mb = b.varray;
+		if (ma == null) {
+			if (sa == 0) {
+				return b;
+			}
+			if (mb == null) {
+				if (sb == 0) {
+					return a;
+				}
+				return new ZInt((long)sa + (long)sb);
+			}
+			ma = new uint[1] { (uint)sa };
+			sa >>= 31;
+		} else if (mb == null) {
+			if (sb == 0) {
+				return a;
+			}
+			mb = new uint[1] { (uint)sb };
+			sb >>= 31;
+		}
+		int na = ma.Length;
+		int nb = mb.Length;
+		int n = Math.Max(na, nb) + 1;
+		uint[] mc = new uint[n];
+		ulong carry = 0;
+		for (int i = 0; i < n; i ++) {
+			uint wa = i < na ? ma[i] : (uint)sa;
+			uint wb = i < nb ? mb[i] : (uint)sb;
+			ulong z = (ulong)wa + (ulong)wb + carry;
+			mc[i] = (uint)z;
+			carry = z >> 32;
+		}
+		return Make((-(int)carry) ^ sa ^ sb, mc);
+	}
+
+	/*
+	 * Subtraction operator.
+	 */
+	public static ZInt operator -(ZInt a, ZInt b)
+	{
+		int sa = a.small;
+		int sb = b.small;
+		uint[] ma = a.varray;
+		uint[] mb = b.varray;
+		if (ma == null) {
+			if (sa == 0) {
+				return -b;
+			}
+			if (mb == null) {
+				if (sb == 0) {
+					return a;
+				}
+				return new ZInt((long)sa - (long)sb);
+			}
+			ma = new uint[1] { (uint)sa };
+			sa >>= 31;
+		} else if (mb == null) {
+			if (sb == 0) {
+				return a;
+			}
+			mb = new uint[1] { (uint)sb };
+			sb >>= 31;
+		}
+		int na = ma.Length;
+		int nb = mb.Length;
+		int n = Math.Max(na, nb) + 1;
+		uint[] mc = new uint[n];
+		long carry = 0;
+		for (int i = 0; i < n; i ++) {
+			uint wa = i < na ? ma[i] : (uint)sa;
+			uint wb = i < nb ? mb[i] : (uint)sb;
+			long z = (long)wa - (long)wb + carry;
+			mc[i] = (uint)z;
+			carry = z >> 32;
+		}
+		return Make((int)carry ^ sa ^ sb, mc);
+	}
+
+	/*
+	 * Increment operator.
+	 */
+	public static ZInt operator ++(ZInt a)
+	{
+		int s = a.small;
+		uint[] ma = a.varray;
+		if (ma == null) {
+			return new ZInt((long)s + 1);
+		}
+		uint[] mb = Dup(ma);
+		if (MutateIncr(mb)) {
+			int n = ma.Length;
+			mb = new uint[n + 1];
+			mb[n] = 1;
+			return new ZInt(0, mb);
+		} else {
+			return Make(s, mb);
+		}
+	}
+
+	/*
+	 * Decrement operator.
+	 */
+	public static ZInt operator --(ZInt a)
+	{
+		int s = a.small;
+		uint[] ma = a.varray;
+		if (ma == null) {
+			return new ZInt((long)s - 1);
+		}
+
+		/*
+		 * MutateDecr() will report a carry only if the varray
+		 * contained only zeros; since this value was not small,
+		 * then it must have been negative.
+		 */
+		uint[] mb = Dup(ma);
+		if (MutateDecr(mb)) {
+			int n = mb.Length;
+			uint[] mc = new uint[n + 1];
+			Array.Copy(mb, 0, mc, 0, n);
+			mc[n] = 0xFFFFFFFE;
+			return new ZInt(-1, mc);
+		} else {
+			return Make(s, mb);
+		}
+	}
+
+	/*
+	 * Multiplication operator.
+	 */
+	public static ZInt operator *(ZInt a, ZInt b)
+	{
+		int sa = a.small;
+		int sb = b.small;
+		uint[] ma = a.varray;
+		uint[] mb = b.varray;
+
+		/*
+		 * Special cases:
+		 * -- one of the operands is zero
+		 * -- both operands are small
+		 */
+		if (ma == null) {
+			if (sa == 0) {
+				return Zero;
+			}
+			if (mb == null) {
+				if (sb == 0) {
+					return Zero;
+				}
+				return new ZInt((long)sa * (long)sb);
+			}
+		} else if (mb == null) {
+			if (sb == 0) {
+				return Zero;
+			}
+		}
+
+		/*
+		 * Get both values in sign+magnitude representation.
+		 */
+		ToAbs(a, out sa, out ma);
+		ToAbs(b, out sb, out mb);
+
+		/*
+		 * Compute the product. Set the sign.
+		 */
+		ZInt r = Make(0, Mul(ma, mb));
+		if ((sa ^ sb) < 0) {
+			r = -r;
+		}
+		return r;
+	}
+
+	/*
+	 * Integer division: the quotient is returned, and the remainder
+	 * is written in 'r'.
+	 *
+	 * This operation follows the C# rules for integer division:
+	 * -- rounding is towards 0
+	 * -- quotient is positive if dividend and divisor have the same
+	 *    sign, negative otherwise
+	 * -- remainder has the sign of the dividend
+	 *
+	 * Attempt at dividing by zero triggers a DivideByZeroException.
+	 */
+	public static ZInt DivRem(ZInt a, ZInt b, out ZInt r)
+	{
+		ZInt q;
+		DivRem(a, b, out q, out r);
+		return q;
+	}
+
+	static void DivRem(ZInt a, ZInt b,
+		out ZInt q, out ZInt r)
+	{
+		int sa = a.small;
+		int sb = b.small;
+		uint[] ma = a.varray;
+		uint[] mb = b.varray;
+
+		/*
+		 * Division by zero triggers an exception.
+		 */
+		if (mb == null && sb == 0) {
+			throw new DivideByZeroException();
+		}
+
+		/*
+		 * If the dividend is zero, then both quotient and
+		 * remainder are zero.
+		 */
+		if (ma == null && sa == 0) {
+			q = Zero;
+			r = Zero;
+			return;
+		}
+
+		/*
+		 * If both dividend and divisor are small, then we
+		 * use the native 64-bit integer types. If only the
+		 * divisor is small, then we have a special fast case
+		 * for division by 1 or -1.
+		 */
+		if (ma == null && mb == null) {
+			q = new ZInt((long)sa / (long)sb);
+			r = new ZInt((long)sa % (long)sb);
+			return;
+		}
+		if (mb == null) {
+			if (sb == 1) {
+				q = a;
+				r = Zero;
+				return;
+			} else if (sb == -1) {
+				q = -a;
+				r = Zero;
+				return;
+			}
+		}
+
+		/*
+		 * We know that the dividend is not 0, and the divisor
+		 * is not -1, 0 or 1. We now want the sign+magnitude
+		 * representations of both operands.
+		 */
+		ToAbs(a, out sa, out ma);
+		ToAbs(b, out sb, out mb);
+
+		/*
+		 * If the divisor is greater (in magnitude) than the
+		 * dividend, then the quotient is zero and the remainder
+		 * is equal to the dividend. If the divisor and dividend
+		 * are equal in magnitude, then the remainder is zero and
+		 * the quotient is 1 if divisor and dividend have the same
+		 * sign, -1 otherwise.
+		 */
+		int cc = Compare(ma, mb);
+		if (cc < 0) {
+			q = Zero;
+			r = a;
+			return;
+		} else if (cc == 0) {
+			q = (sa == sb) ? One : MinusOne;
+			r = Zero;
+			return;
+		}
+
+		/*
+		 * At that point, we know that the divisor is not -1, 0
+		 * or 1, and that the quotient will not be 0. We perform
+		 * the unsigned division (with the magnitudes), then
+		 * we adjust the signs.
+		 */
+
+		uint[] mq, mr;
+		DivRem(ma, mb, out mq, out mr);
+
+		/*
+		 * Quotient is positive if divisor and dividend have the
+		 * same sign, negative otherwise. Remainder always has
+		 * the sign of the dividend, but it may be zero.
+		 */
+		q = Make(0, mq);
+		if (sa != sb) {
+			q = -q;
+		}
+		r = Make(0, mr);
+		if (sa < 0) {
+			r = -r;
+		}
+#if DEBUG
+		if (q * b + r != a) {
+			throw new Exception("division error");
+		}
+#endif
+	}
+
+	/*
+	 * Division operator: see DivRem() for details.
+	 */
+	public static ZInt operator /(ZInt a, ZInt b)
+	{
+		ZInt q, r;
+		DivRem(a, b, out q, out r);
+		return q;
+	}
+
+	/*
+	 * Remainder operator: see DivRem() for details.
+	 */
+	public static ZInt operator %(ZInt a, ZInt b)
+	{
+		ZInt q, r;
+		DivRem(a, b, out q, out r);
+		return r;
+	}
+
+	/*
+	 * Reduce this value modulo the provided m. This differs from
+	 * '%' in that the returned value is always in the 0 to abs(m)-1
+	 * range.
+	 */
+	public ZInt Mod(ZInt m)
+	{
+		ZInt r = this % m;
+		if (r.small < 0) {
+			if (m.small < 0) {
+				m = -m;
+			}
+			r += m;
+		}
+		return r;
+	}
+
+	/*
+	 * Left-shift operator.
+	 */
+	public static ZInt operator <<(ZInt a, int n)
+	{
+		if (n < 0) {
+			if (n == Int32.MinValue) {
+				return Zero;
+			}
+			return a >> (-n);
+		}
+		int sa = a.small;
+		uint[] ma = a.varray;
+		if (ma == null) {
+			if (n <= 32) {
+				return new ZInt((long)sa << n);
+			}
+			if (sa == 0) {
+				return Zero;
+			}
+		}
+		uint[] mr = new uint[GetLengthForLeftShift(a, n)];
+
+		/*
+		 * Special case when the shift is a multiple of 32.
+		 */
+		int kw = n >> 5;
+		int kb = n & 31;
+		if (kb == 0) {
+			if (ma == null) {
+				mr[kw] = (uint)sa;
+				return new ZInt(sa >> 31, mr);
+			} else {
+				Array.Copy(ma, 0, mr, kw, ma.Length);
+				return new ZInt(sa, mr);
+			}
+		}
+
+		/*
+		 * At that point, we know that the source integer does
+		 * not fit in a signed "int", or is shifted by more than
+		 * 32 bits, or both. Either way, the result will not fit
+		 * in an "int".
+		 *
+		 * We process all input words one by one.
+		 */
+		uint rem = 0;
+		int ikb = 32 - kb;
+		int j;
+		if (ma == null) {
+			j = 1;
+			uint wa = (uint)sa;
+			mr[kw] = wa << kb;
+			rem = wa >> ikb;
+		} else {
+			j = ma.Length;
+			for (int i = 0; i < j; i ++) {
+				uint wa = ma[i];
+				mr[i + kw] = rem | (wa << kb);
+				rem = wa >> ikb;
+			}
+		}
+		sa >>= 31;
+#if DEBUG
+		if ((j + kw) == mr.Length - 1) {
+			if (rem == ((uint)sa >> ikb)) {
+				throw new Exception(
+					"Wrong left-shift: untrimmed");
+			}
+		} else if ((j + kw) == mr.Length) {
+			if (rem != ((uint)sa >> ikb)) {
+				throw new Exception(
+					"Wrong left-shift: dropped bits");
+			}
+		} else {
+			throw new Exception(
+				"Wrong left-shift: oversized output length");
+		}
+#endif
+		if ((j + kw) < mr.Length) {
+			mr[j + kw] = rem | ((uint)sa << kb);
+		}
+		return new ZInt(sa, mr);
+	}
+
+	/*
+	 * Right-shift operator.
+	 */
+	public static ZInt operator >>(ZInt a, int n)
+	{
+		if (n < 0) {
+			if (n == Int32.MinValue) {
+				throw new OverflowException();
+			}
+			return a << (-n);
+		}
+		int sa = a.small;
+		uint[] ma = a.varray;
+		if (ma == null) {
+			/*
+			 * If right-shifting a "small" value, then we can
+			 * do the computation with the native ">>" operator
+			 * on "int" values, unless the shift count is 32
+			 * or more, in which case we get either 0 or -1,
+			 * depending on the source value sign.
+			 */
+			if (n < 32) {
+				return new ZInt(sa >> n, null);
+			} else {
+				return new ZInt(sa >> 31, null);
+			}
+		}
+
+		/*
+		 * At that point, we know that the source value uses
+		 * a non-null varray. We compute the bit length of the
+		 * result. If the result would fit in an "int" (bit length
+		 * of 31 or less) then we handle it as a special case.
+		 */
+		int kw = n >> 5;
+		int kb = n & 31;
+		int bl = a.BitLength - n;
+		if (bl <= 0) {
+			return new ZInt(sa, null);
+		} else if (bl <= 31) {
+			if (kb == 0) {
+				return new ZInt((int)ma[kw], null);
+			} else {
+				int p = ma.Length;
+				uint w0 = ma[kw];
+				uint w1 = (kw + 1) < p ? ma[kw + 1] : (uint)sa;
+				return new ZInt((int)((w0 >> kb)
+					| (w1 << (32 - kb))), null);
+			}
+		}
+
+		/*
+		 * Result will require an array. Let's allocate it.
+		 */
+		uint[] mr = new uint[(bl + 31) >> 5];
+
+		/*
+		 * Special case when the shift is a multiple of 32.
+		 */
+		if (kb == 0) {
+#if DEBUG
+			if (mr.Length != (ma.Length - kw)) {
+				throw new Exception(
+					"Wrong right-shift: output length");
+			}
+#endif
+			Array.Copy(ma, kw, mr, 0, ma.Length - kw);
+			return new ZInt(sa, mr);
+		}
+
+		/*
+		 * We process all input words one by one.
+		 */
+		int ikb = 32 - kb;
+		uint rem = ma[kw] >> kb;
+		int j = ma.Length;
+		for (int i = kw + 1; i < j; i ++) {
+			uint wa = ma[i];
+			mr[i - kw - 1] = rem | (wa << ikb);
+			rem = wa >> kb;
+		}
+#if DEBUG
+		if ((j - kw - 1) == mr.Length - 1) {
+			if (rem == ((uint)sa >> kb)) {
+				throw new Exception(
+					"Wrong right-shift: untrimmed");
+			}
+		} else if ((j - kw - 1) == mr.Length) {
+			if (rem != ((uint)sa >> kb)) {
+				throw new Exception(
+					"Wrong right-shift: dropped bits");
+			}
+		} else {
+			throw new Exception(
+				"Wrong right-shift: oversized output length");
+		}
+#endif
+		if ((j - kw - 1) < mr.Length) {
+			mr[j - kw - 1] = rem | ((uint)sa << ikb);
+		}
+		return new ZInt(sa, mr);
+	}
+
+	/*
+	 * NOTES ON BITWISE BOOLEAN OPERATIONS
+	 *
+	 * When both operands are "small" then the result is necessarily
+	 * small: in "small" encoding, all bits beyond bit 31 of the
+	 * two's complement encoding are equal to bit 31, so the result
+	 * of computing the operation on bits 31 will also be valid for
+	 * all subsequent bits. Therefore, when the two operands are
+	 * small, we can just do the operation on the "int" values and the
+	 * result is guaranteed "small" as well.
+	 */
+
+	/*
+	 * Compute the bitwise AND between a "big" and a "small" values.
+	 */
+	static ZInt AndSmall(int s, uint[] m, int x)
+	{
+		if (x < 0) {
+			uint[] r = Dup(m);
+			r[0] &= (uint)x;
+			return new ZInt(s, r);
+		} else {
+			return new ZInt(x & (int)m[0], null);
+		}
+	}
+
+	/*
+	 * Bitwise AND operator.
+	 */
+	public static ZInt operator &(ZInt a, ZInt b)
+	{
+		int sa = a.small;
+		int sb = b.small;
+		uint[] ma = a.varray;
+		uint[] mb = b.varray;
+		if (ma == null) {
+			if (mb == null) {
+				return new ZInt(sa & sb, null);
+			} else {
+				return AndSmall(sb, mb, sa);
+			}
+		} else if (mb == null) {
+			return AndSmall(sa, ma, sb);
+		}
+
+		/*
+		 * Both values are big. Since upper zero bits force the
+		 * result to contain zeros, the result size is that of the
+		 * positive operand (the smallest of the two if both are
+		 * positive). If both operands are negative, then the
+		 * result magnitude may be as large as the largest of the
+		 * two source magnitudes.
+		 *
+		 * Result is negative if and only if both operands are
+		 * negative.
+		 */
+		int na = ma.Length;
+		int nb = mb.Length;
+		int nr;
+		if (sa >= 0) {
+			if (sb >= 0) {
+				nr = Math.Min(na, nb);
+			} else {
+				nr = na;
+			}
+		} else {
+			if (sb >= 0) {
+				nr = nb;
+			} else {
+				nr = Math.Max(na, nb);
+			}
+		}
+		uint[] mr = new uint[nr];
+		for (int i = 0; i < nr; i ++) {
+			uint wa = i < na ? ma[i] : (uint)sa;
+			uint wb = i < nb ? mb[i] : (uint)sb;
+			mr[i] = wa & wb;
+		}
+		return Make(sa & sb, mr);
+	}
+
+	/*
+	 * Compute the bitwise OR between a "big" value (sign and
+	 * magnitude, already normalized/trimmed), and a small value
+	 * (which can be positive or negative).
+	 */
+	static ZInt OrSmall(int s, uint[] m, int x)
+	{
+		if (x < 0) {
+			return new ZInt(x | (int)m[0], null);
+		} else {
+			uint[] r = Dup(m);
+			r[0] |= (uint)x;
+			return new ZInt(s, r);
+		}
+	}
+
+	/*
+	 * Bitwise OR operator.
+	 */
+	public static ZInt operator |(ZInt a, ZInt b)
+	{
+		int sa = a.small;
+		int sb = b.small;
+		uint[] ma = a.varray;
+		uint[] mb = b.varray;
+		if (ma == null) {
+			if (mb == null) {
+				return new ZInt(sa | sb, null);
+			} else {
+				return OrSmall(sb, mb, sa);
+			}
+		} else if (mb == null) {
+			return OrSmall(sa, ma, sb);
+		}
+
+		/*
+		 * Both values are big. Since upper one bits force the
+		 * result to contain ones, the result size is that of
+		 * the negative operand (the greater, i.e. "smallest" of
+		 * the two if both are negative). If both operands are
+		 * positive, then the result magnitude may be as large
+		 * as the largest of the two source magnitudes.
+		 *
+		 * Result is positive if and only if both operands are
+		 * positive.
+		 */
+		int na = ma.Length;
+		int nb = mb.Length;
+		int nr;
+		if (sa >= 0) {
+			if (sb >= 0) {
+				nr = Math.Max(na, nb);
+			} else {
+				nr = nb;
+			}
+		} else {
+			if (sb >= 0) {
+				nr = na;
+			} else {
+				nr = Math.Min(na, nb);
+			}
+		}
+		uint[] mr = new uint[nr];
+		for (int i = 0; i < nr; i ++) {
+			uint wa = i < na ? ma[i] : (uint)sa;
+			uint wb = i < nb ? mb[i] : (uint)sb;
+			mr[i] = wa | wb;
+		}
+		return Make(sa | sb, mr);
+	}
+
+	/*
+	 * Bitwise XOR operator.
+	 */
+	public static ZInt operator ^(ZInt a, ZInt b)
+	{
+		int sa = a.small;
+		int sb = b.small;
+		uint[] ma = a.varray;
+		uint[] mb = b.varray;
+		if (ma == null && mb == null) {
+			return new ZInt(sa ^ sb, null);
+		}
+		if (ma == null) {
+			int st = sa;
+			sa = sb;
+			sb = st;
+			uint[] mt = ma;
+			ma = mb;
+			mb = mt;
+		}
+		if (mb == null) {
+			int nx = ma.Length;
+			uint[] mx = new uint[nx];
+			mx[0] = ma[0] ^ (uint)sb;
+			if (nx > 1) {
+				if (sb < 0) {
+					for (int i = 1; i < nx; i ++) {
+						mx[i] = ~ma[i];
+					}
+				} else {
+					Array.Copy(ma, 1, mx, 1, nx - 1);
+				}
+			}
+			return Make(sa ^ (sb >> 31), mx);
+		}
+
+		/*
+		 * Both operands use varrays.
+		 * Result can be as big as the bigger of the two operands
+		 * (it _will_ be that big, necessarily, if the two operands
+		 * have distinct sizes).
+		 */
+		int na = ma.Length;
+		int nb = mb.Length;
+		int nr = Math.Max(na, nb);
+		uint[] mr = new uint[nr];
+		for (int i = 0; i < nr; i ++) {
+			uint wa = i < na ? ma[i] : (uint)sa;
+			uint wb = i < nb ? mb[i] : (uint)sb;
+			mr[i] = wa ^ wb;
+		}
+		return Make((sa ^ sb) >> 31, mr);
+	}
+
+	/*
+	 * Bitwise inversion operator.
+	 */
+	public static ZInt operator ~(ZInt a)
+	{
+		int s = a.small;
+		uint[] m = a.varray;
+		if (m == null) {
+			return new ZInt(~s, null);
+		} else {
+			int n = m.Length;
+			uint[] r = new uint[n];
+			for (int i = 0; i < n; i ++) {
+				r[i] = ~m[i];
+			}
+			return new ZInt(~s, r);
+		}
+	}
+
+	/*
+	 * Basic comparison; returned value is -1, 0 or 1 depending on
+	 * whether this instance is to be considered lower then, equal
+	 * to, or greater than the provided object.
+	 *
+	 * All ZInt instances are considered greater than 'null', and
+	 * lower than any non-null object that is not a ZInt.
+	 */
+	public int CompareTo(object obj)
+	{
+		if (obj == null) {
+			return 1;
+		}
+		if (!(obj is ZInt)) {
+			return -1;
+		}
+		return CompareTo((ZInt)obj);
+	}
+
+	/*
+	 * Basic comparison; returned value is -1, 0 or 1 depending on
+	 * whether this instance is to be considered lower then, equal
+	 * to, or greater than the provided value.
+	 */
+	public int CompareTo(ZInt v)
+	{
+		int sv = v.small;
+		uint[] mv = v.varray;
+		int sign1 = small >> 31;
+		int sign2 = sv >> 31;
+		if (sign1 != sign2) {
+			/*
+			 * One of the sign* values is -1, the other is 0.
+			 */
+			return sign1 - sign2;
+		}
+
+		/*
+		 * Both values have the same sign. Since the varrays are
+		 * trimmed, we can use their presence and length to
+		 * quickly resolve most cases.
+		 */
+		if (small < 0) {
+			if (varray == null) {
+				if (mv == null) {
+					return Compare(small, sv);
+				} else {
+					return 1;
+				}
+			} else {
+				if (mv == null) {
+					return -1;
+				} else {
+					int n1 = varray.Length;
+					int n2 = mv.Length;
+					if (n1 < n2) {
+						return 1;
+					} else if (n1 == n2) {
+						return Compare(varray, mv);
+					} else {
+						return -1;
+					}
+				}
+			}
+		} else {
+			if (varray == null) {
+				if (mv == null) {
+					return Compare(small, sv);
+				} else {
+					return -1;
+				}
+			} else {
+				if (mv == null) {
+					return 1;
+				} else {
+					return Compare(varray, mv);
+				}
+			}
+		}
+	}
+
+	/*
+	 * Equality comparison: a ZInt instance is equal only to another
+	 * ZInt instance that encodes the same integer value.
+	 */
+	public override bool Equals(object obj)
+	{
+		if (obj == null) {
+			return false;
+		}
+		if (!(obj is ZInt)) {
+			return false;
+		}
+		return CompareTo((ZInt)obj) == 0;
+	}
+
+	/*
+	 * Equality comparison: a ZInt instance is equal only to another
+	 * ZInt instance that encodes the same integer value.
+	 */
+	public bool Equals(ZInt v)
+	{
+		return CompareTo(v) == 0;
+	}
+
+	/*
+	 * The hash code for a ZInt is equal to its lower 32 bits.
+	 */
+	public override int GetHashCode()
+	{
+		if (varray == null) {
+			return small;
+		} else {
+			return (int)varray[0];
+		}
+	}
+
+	/*
+	 * Equality operator.
+	 */
+	public static bool operator ==(ZInt a, ZInt b)
+	{
+		return a.CompareTo(b) == 0;
+	}
+
+	/*
+	 * Inequality operator.
+	 */
+	public static bool operator !=(ZInt a, ZInt b)
+	{
+		return a.CompareTo(b) != 0;
+	}
+
+	/*
+	 * Lower-than operator.
+	 */
+	public static bool operator <(ZInt a, ZInt b)
+	{
+		return a.CompareTo(b) < 0;
+	}
+
+	/*
+	 * Lower-or-equal operator.
+	 */
+	public static bool operator <=(ZInt a, ZInt b)
+	{
+		return a.CompareTo(b) <= 0;
+	}
+
+	/*
+	 * Greater-than operator.
+	 */
+	public static bool operator >(ZInt a, ZInt b)
+	{
+		return a.CompareTo(b) > 0;
+	}
+
+	/*
+	 * Greater-or-equal operator.
+	 */
+	public static bool operator >=(ZInt a, ZInt b)
+	{
+		return a.CompareTo(b) >= 0;
+	}
+
+	/*
+	 * Power function: this raises x to the power e. The exponent e
+	 * MUST NOT be negative. If x and e are both zero, then 1 is
+	 * returned.
+	 */
+	public static ZInt Pow(ZInt x, int e)
+	{
+		if (e < 0) {
+			throw new ArgumentOutOfRangeException();
+		}
+		if (e == 0) {
+			return One;
+		}
+		if (e == 1 || x.IsZero || x.IsOne) {
+			return x;
+		}
+		bool neg = false;
+		if (x.Sign < 0) {
+			x = -x;
+			neg = (e & 1) != 0;
+		}
+		if (x.IsPowerOfTwo) {
+			int t = x.BitLength - 1;
+			long u = (long)t * (long)e;
+			if (u > (long)Int32.MaxValue) {
+				throw new OverflowException();
+			}
+			x = One << (int)u;
+		} else {
+			ZInt y = One;
+			for (;;) {
+				if ((e & 1) != 0) {
+					y *= x;
+				}
+				e >>= 1;
+				if (e == 0) {
+					break;
+				}
+				x *= x;
+			}
+			x = y;
+		}
+		return neg ? -x : x;
+	}
+
+	/*
+	 * Modular exponentation: this function raises v to the power e
+	 * modulo m. The returned value is reduced modulo m: it will be
+	 * in the 0 to abs(m)-1 range.
+	 *
+	 * The modulus m must be positive. If m is 1, then the result is
+	 * 0 (regardless of the values of v and e).
+	 *
+	 * The exponent e must be nonnegative (this function does not
+	 * compute modular inverses). If e is zero, then the result is 1
+	 * (except if m is 1).
+	 */
+	public static ZInt ModPow(ZInt v, ZInt e, ZInt m)
+	{
+		int se = e.Sign;
+		if (se < 0) {
+			throw new ArgumentOutOfRangeException();
+		}
+		int sm = m.Sign;
+		if (sm < 0) {
+			m = -m;
+		} else if (sm == 0) {
+			throw new DivideByZeroException();
+		}
+		if (m.varray == null && m.small == 1) {
+			return Zero;
+		}
+		if (se == 0) {
+			return One;
+		}
+		if (v.IsZero) {
+			return Zero;
+		}
+
+		// TODO: use Montgomery's multiplication when the exponent
+		// is large.
+		ZInt x = v.Mod(m);
+		for (int n = e.BitLength - 2; n >= 0; n --) {
+			x = (x * x).Mod(m);
+			if (e.TestBit(n)) {
+				x = (x * v).Mod(m);
+			}
+		}
+		return x;
+	}
+
+	/*
+	 * Get the absolute value of a ZInt.
+	 */
+	public static ZInt Abs(ZInt x)
+	{
+		return (x.Sign < 0) ? -x : x;
+	}
+
+	private static void AppendHex(StringBuilder sb, uint w, bool trim)
+	{
+		int i = 28;
+		if (trim) {
+			for (; i >= 0 && (w >> i) == 0; i -= 4);
+			if (i < 0) {
+				sb.Append("0");
+				return;
+			}
+		}
+		for (; i >= 0; i -= 4) {
+			sb.Append("0123456789ABCDEF"[(int)((w >> i) & 0x0F)]);
+		}
+	}
+
+	/*
+	 * Convert this value to hexadecimal. If this instance is zero,
+	 * then "0" is returned. Otherwise, the number of digits is
+	 * minimal (no leading '0'). A leading '-' is used for negative
+	 * values. Hexadecimal digits are uppercase.
+	 */
+	public string ToHexString()
+	{
+		if (varray == null && small == 0) {
+			return "0";
+		}
+		StringBuilder sb = new StringBuilder();
+		ZInt x = this;
+		if (x.small < 0) {
+			sb.Append('-');
+			x = -x;
+		}
+		if (x.varray == null) {
+			AppendHex(sb, (uint)x.small, true);
+		} else {
+			int n = x.varray.Length;
+			AppendHex(sb, x.varray[n - 1], true);
+			for (int j = n - 2; j >= 0; j --) {
+				AppendHex(sb, x.varray[j], false);
+			}
+		}
+		return sb.ToString();
+	}
+
+	/*
+	 * Convert this value to decimal. A leading '-' sign is used for
+	 * negative value. The number of digits is minimal (no leading '0',
+	 * except for zero, which is returned as "0").
+	 */
+	public override string ToString()
+	{
+		return ToString(10);
+	}
+
+	private static int[] NDIGITS32 = {
+		0, 0, 31, 20, 15, 13, 12, 11, 10, 10, 9, 9, 8, 8, 8, 8, 7, 7,
+		7, 7, 7, 7, 7, 7, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6
+	};
+
+	private static uint[] RADIXPP32 = {
+		0, 0, 2147483648, 3486784401, 1073741824, 1220703125,
+		2176782336, 1977326743, 1073741824, 3486784401, 1000000000,
+		2357947691, 429981696, 815730721, 1475789056, 2562890625,
+		268435456, 410338673, 612220032, 893871739, 1280000000,
+		1801088541, 2494357888, 3404825447, 191102976, 244140625,
+		308915776, 387420489, 481890304, 594823321, 729000000,
+		887503681, 1073741824, 1291467969, 1544804416, 1838265625,
+		2176782336
+	};
+
+	private static char[] DCHAR =
+		"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ".ToCharArray();
+
+	static void AppendDigits(StringBuilder sb, uint v, int radix, int num)
+	{
+		while (num -- > 0) {
+			sb.Append(DCHAR[v % (uint)radix]);
+			v = v / (uint)radix;
+		}
+	}
+
+	static void AppendDigits(StringBuilder sb, uint v, int radix)
+	{
+		while (v > 0) {
+			sb.Append(DCHAR[v % (uint)radix]);
+			v = v / (uint)radix;
+		}
+	}
+
+	/*
+	 * Convert this value to a string, in the provided radix. The
+	 * radix must be in the 2 to 36 range (inclusive); uppercase
+	 * letters 'A' to 'Z' are used for digits of value 10 to 35.
+	 * If the value is zero, then "0" is returned; otherwise, leading
+	 * '0' digits are removed. A leading '-' sign is added for negative
+	 * values.
+	 */
+	public string ToString(int radix)
+	{
+		if (radix < 2 || radix > 36) {
+			throw new ArgumentOutOfRangeException();
+		}
+
+		/*
+		 * Special optimized case for base 16.
+		 */
+		if (radix == 16) {
+			return ToHexString();
+		}
+
+		if (IsZero) {
+			return "0";
+		}
+
+		ZInt x = this;
+		if (x.Sign < 0) {
+			x = -x;
+		}
+		StringBuilder sb = new StringBuilder();
+		if (x.varray == null) {
+			AppendDigits(sb, (uint)x.small, radix);
+		} else {
+			uint[] m = new uint[x.varray.Length];
+			Array.Copy(x.varray, 0, m, 0, m.Length);
+			uint prad = RADIXPP32[radix];
+			int dnum = NDIGITS32[radix];
+			for (;;) {
+				uint v = MutateDivRem(m, prad);
+				bool qz = Length(0, m) == 0;
+				if (qz) {
+					AppendDigits(sb, v, radix);
+					break;
+				} else {
+					AppendDigits(sb, v, radix, dnum);
+				}
+			}
+		}
+		if (Sign < 0) {
+			sb.Append('-');
+		}
+		return Reverse(sb);
+	}
+
+	static string Reverse(StringBuilder sb)
+	{
+		int n = sb.Length;
+		char[] tc = new char[n];
+		sb.CopyTo(0, tc, 0, n);
+		for (int i = 0, j = n - 1; i < j; i ++, j --) {
+			char c = tc[i];
+			tc[i] = tc[j];
+			tc[j] = c;
+		}
+		return new string(tc);
+	}
+
+	static uint DigitValue(char c, int radix)
+	{
+		int d;
+		if (c >= '0' && c <= '9') {
+			d = c - '0';
+		} else if (c >= 'a' && c <= 'z') {
+			d = (c - 'a') + 10;
+		} else if (c >= 'A' && c <= 'Z') {
+			d = (c - 'A') + 10;
+		} else {
+			d = -1;
+		}
+		if (d < 0 || d >= radix) {
+			throw new ArgumentException();
+		}
+		return (uint)d;
+	}
+
+	static ZInt ParseUnsigned(string s, int radix)
+	{
+		if (s.Length == 0) {
+			throw new ArgumentException();
+		}
+		ZInt x = Zero;
+		uint acc = 0;
+		int accNum = 0;
+		uint prad = RADIXPP32[radix];
+		int dnum = NDIGITS32[radix];
+		foreach (char c in s) {
+			uint d = DigitValue(c, radix);
+			acc = acc * (uint)radix + d;
+			if (++ accNum == dnum) {
+				x = x * (ZInt)prad + (ZInt)acc;
+				acc = 0;
+				accNum = 0;
+			}
+		}
+		if (accNum > 0) {
+			uint p = 1;
+			while (accNum -- > 0) {
+				p *= (uint)radix;
+			}
+			x = x * (ZInt)p + (ZInt)acc;
+		}
+		return x;
+	}
+
+	/*
+	 * Parse a string:
+	 * -- A leading '-' is allowed, to denote a negative value.
+	 * -- If there is a "0b" or "0B" header (after any '-' sign),
+	 *    then the value is interpreted in base 2.
+	 * -- If there is a "0x" or "0X" header (after any '-' sign),
+	 *    then the value is interpreted in base 16 (hexadecimal).
+	 *    Both uppercase and lowercase letters are accepted.
+	 * -- If there is no header, then decimal interpretation is used.
+	 *
+	 * Unexpected characters (including spaces) trigger exceptions.
+	 * There must be at least one digit.
+	 */
+	public static ZInt Parse(string s)
+	{
+		s = s.Trim();
+		bool neg = false;
+		if (s.StartsWith("-")) {
+			neg = true;
+			s = s.Substring(1);
+		}
+		int radix;
+		if (s.StartsWith("0b") || s.StartsWith("0B")) {
+			radix = 2;
+			s = s.Substring(2);
+		} else if (s.StartsWith("0x") || s.StartsWith("0X")) {
+			radix = 16;
+			s = s.Substring(2);
+		} else {
+			radix = 10;
+		}
+		ZInt x = ParseUnsigned(s, radix);
+		return neg ? -x : x;
+	}
+
+	/*
+	 * Parse a string in the specified radix. The radix must be in
+	 * the 2 to 36 range (inclusive). Uppercase and lowercase letters
+	 * are accepted for digits in the 10 to 35 range.
+	 *
+	 * A leading '-' sign is allowed, to denote a negative value.
+	 * Otherwise, only digits (acceptable with regards to the radix)
+	 * may appear. There must be at least one digit.
+	 */
+	public static ZInt Parse(string s, int radix)
+	{
+		if (radix < 2 || radix > 36) {
+			throw new ArgumentOutOfRangeException();
+		}
+		s = s.Trim();
+		bool neg = false;
+		if (s.StartsWith("-")) {
+			neg = true;
+			s = s.Substring(1);
+		}
+		ZInt x = ParseUnsigned(s, radix);
+		return neg ? -x : x;
+	}
+
+	static uint DecU32BE(byte[] buf, int off, int len)
+	{
+		switch (len) {
+		case 0:
+			return 0;
+		case 1:
+			return buf[off];
+		case 2:
+			return ((uint)buf[off] << 8)
+				| (uint)buf[off + 1];
+		case 3:
+			return ((uint)buf[off] << 16)
+				| ((uint)buf[off + 1] << 8)
+				| (uint)buf[off + 2];
+		default:
+			return ((uint)buf[off] << 24)
+				| ((uint)buf[off + 1] << 16)
+				| ((uint)buf[off + 2] << 8)
+				| (uint)buf[off + 3];
+		}
+	}
+
+	/*
+	 * Decode an integer, assuming unsigned big-endian encoding.
+	 * An empty array is decoded as 0.
+	 */
+	public static ZInt DecodeUnsignedBE(byte[] buf)
+	{
+		return DecodeUnsignedBE(buf, 0, buf.Length);
+	}
+
+	/*
+	 * Decode an integer, assuming unsigned big-endian encoding.
+	 * An empty array is decoded as 0.
+	 */
+	public static ZInt DecodeUnsignedBE(byte[] buf, int off, int len)
+	{
+		while (len > 0 && buf[off] == 0) {
+			off ++;
+			len --;
+		}
+		if (len == 0) {
+			return Zero;
+		} else if (len <= 4) {
+			return new ZInt(DecU32BE(buf, off, len));
+		}
+		uint[] m = new uint[(len + 3) >> 2];
+		int i = 0;
+		for (int j = len; j > 0; j -= 4) {
+			int k = j - 4;
+			uint w;
+			if (k < 0) {
+				w = DecU32BE(buf, off, j);
+			} else {
+				w = DecU32BE(buf, off + k, 4);
+			}
+			m[i ++] = w;
+		}
+		return new ZInt(0, m);
+	}
+
+	static RNGCryptoServiceProvider RNG = new RNGCryptoServiceProvider();
+
+	/*
+	 * Create a random integer of the provided size. Returned value
+	 * is in the 0 (inclusive) to 2^size (exclusive) range. A
+	 * cryptographically strong RNG is used to ensure uniform selection.
+	 */
+	public static ZInt MakeRand(int size)
+	{
+		if (size <= 0) {
+			throw new ArgumentOutOfRangeException();
+		}
+		byte[] buf = new byte[(size + 7) >> 3];
+		RNG.GetBytes(buf);
+		int kb = size & 7;
+		if (kb != 0) {
+			buf[0] &= (byte)(0xFF >> (8 - kb));
+		}
+		return DecodeUnsignedBE(buf);
+	}
+
+	/*
+	 * Create a random integer in the 0 (inclusive) to max (exclusive)
+	 * range. 'max' must be positive. A cryptographically strong RNG
+	 * is used to ensure uniform selection.
+	 */
+	public static ZInt MakeRand(ZInt max)
+	{
+		if (max.Sign <= 0) {
+			throw new ArgumentOutOfRangeException();
+		}
+		int bl = max.BitLength;
+		for (;;) {
+			ZInt x = MakeRand(bl);
+			if (x < max) {
+				return x;
+			}
+		}
+	}
+
+	/*
+	 * Create a random integer in the min (inclusive) to max (exclusive)
+	 * range. 'max' must be greater than min. A cryptographically
+	 * strong RNG is used to ensure uniform selection.
+	 */
+	public static ZInt MakeRand(ZInt min, ZInt max)
+	{
+		if (max <= min) {
+			throw new ArgumentOutOfRangeException();
+		}
+		return min + MakeRand(max - min);
+	}
+
+	/*
+	 * Check whether this integer is prime. A probabilistic algorithm
+	 * is used, that theoretically ensures that a non-prime won't be
+	 * declared prime with probability greater than 2^(-100). Note that
+	 * this holds regardless of how the integer was generated (this
+	 * method does not assume that uniform random selection was used).
+	 *
+	 * (Realistically, the probability of a computer hardware
+	 * malfunction is way greater than 2^(-100), so this property
+	 * returns the primality status with as much certainty as can be
+	 * achieved with a computer.)
+	 */
+	public bool IsPrime {
+		get {
+			return IsProbablePrime(50);
+		}
+	}
+
+	static uint[] PRIMES_BF = new uint[] {
+		0xA08A28AC, 0x28208A20, 0x02088288, 0x800228A2,
+		0x20A00A08, 0x80282088, 0x800800A2, 0x08028228,
+		0x0A20A082, 0x22880020, 0x28020800, 0x88208082,
+		0x02022020, 0x08828028, 0x8008A202, 0x20880880
+	};
+
+	private bool IsProbablePrime(int rounds)
+	{
+		ZInt x = this;
+		int cc = x.Sign;
+		if (cc == 0) {
+			return false;
+		} else if (cc < 0) {
+			x = -x;
+		}
+		if (x.varray == null) {
+			if (x.small < (PRIMES_BF.Length << 5)) {
+				return (PRIMES_BF[x.small >> 5]
+					& ((uint)1 << (x.small & 31))) != 0;
+			}
+		}
+		if (!x.TestBit(0)) {
+			return false;
+		}
+
+		ZInt xm1 = x;
+		xm1 --;
+		ZInt m = xm1;
+		int a;
+		for (a = 0; !m.TestBit(a); a ++);
+		m >>= a;
+		while (rounds -- > 0) {
+			ZInt b = MakeRand(Two, x);
+			ZInt z = ModPow(b, m, x);
+			for (int j = 0; j < a; j ++) {
+				if (z == One) {
+					if (j > 0) {
+						return false;
+					}
+					break;
+				}
+				if (z == xm1) {
+					break;
+				}
+				if ((j + 1) < a) {
+					z = (z * z) % x;
+				} else {
+					return false;
+				}
+			}
+		}
+		return true;
+	}
+
+	/*
+	 * Encode this integer as bytes (signed big-endian convention).
+	 * Encoding is of minimal length that still contains a sign bit
+	 * (compatible with ASN.1 DER encoding).
+	 */
+	public byte[] ToBytesBE()
+	{
+		byte[] r = new byte[(BitLength + 8) >> 3];
+		ToBytesBE(r, 0, r.Length);
+		return r;
+	}
+
+	/*
+	 * Encode this integer as bytes (signed little-endian convention).
+	 * Encoding is of minimal length that still contains a sign bit.
+	 */
+	public byte[] ToBytesLE()
+	{
+		byte[] r = new byte[(BitLength + 8) >> 3];
+		ToBytesLE(r, 0, r.Length);
+		return r;
+	}
+
+	/*
+	 * Encode this integer as bytes (signed big-endian convention).
+	 * Output length is provided; exactly that many bytes will be
+	 * written. The value is sign-extended or truncated if needed.
+	 */
+	public void ToBytesBE(byte[] buf, int off, int len)
+	{
+		ToBytes(true, buf, off, len);
+	}
+
+	/*
+	 * Encode this integer as bytes (signed little-endian convention).
+	 * Output length is provided; exactly that many bytes will be
+	 * written. The value is sign-extended or truncated if needed.
+	 */
+	public void ToBytesLE(byte[] buf, int off, int len)
+	{
+		ToBytes(false, buf, off, len);
+	}
+
+	/*
+	 * Encode this integer as bytes (unsigned big-endian convention).
+	 * Encoding is of minimal length, possibly without a sign bit. If
+	 * this value is zero, then an empty array is returned. If this
+	 * value is negative, then an ArgumentOutOfRangeException is thrown.
+	 */
+	public byte[] ToBytesUnsignedBE()
+	{
+		if (Sign < 0) {
+			throw new ArgumentOutOfRangeException();
+		}
+		byte[] r = new byte[(BitLength + 7) >> 3];
+		ToBytesBE(r, 0, r.Length);
+		return r;
+	}
+
+	/*
+	 * Encode this integer as bytes (unsigned little-endian convention).
+	 * Encoding is of minimal length, possibly without a sign bit. If
+	 * this value is zero, then an empty array is returned. If this
+	 * value is negative, then an ArgumentOutOfRangeException is thrown.
+	 */
+	public byte[] ToBytesUnsignedLE()
+	{
+		if (Sign < 0) {
+			throw new ArgumentOutOfRangeException();
+		}
+		byte[] r = new byte[(BitLength + 7) >> 3];
+		ToBytesLE(r, 0, r.Length);
+		return r;
+	}
+
+	void ToBytes(bool be, byte[] buf, int off, int len)
+	{
+		uint iw = (uint)small >> 31;
+		for (int i = 0; i < len; i ++) {
+			int j = i >> 2;
+			uint w;
+			if (varray == null) {
+				w = (j == 0) ? (uint)small : iw;
+			} else {
+				w = (j < varray.Length) ? varray[j] : iw;
+			}
+			byte v = (byte)(w >> ((i & 3) << 3));
+			if (be) {
+				buf[off + len - 1 - i] = v;
+			} else {
+				buf[off + i] = v;
+			}
+		}
+	}
+}
diff --git a/build.cmd b/build.cmd
new file mode 100644
index 0000000..62d86fb
--- /dev/null
+++ b/build.cmd
@@ -0,0 +1,5 @@
+echo "Twrch..."
+%SystemRoot%\Microsoft.NET\Framework\v4.0.30319\csc.exe /target:exe /out:Twrch.exe /main:Twrch Asn1\*.cs Crypto\*.cs SSLTLS\*.cs X500\*.cs XKeys\*.cs Twrch\*.cs
+
+echo "TestCrypto..."
+%SystemRoot%\Microsoft.NET\Framework\v4.0.30319\csc.exe /target:exe /out:TestCrypto.exe /main:TestCrypto Tests\*.cs Asn1\*.cs Crypto\*.cs SSLTLS\*.cs X500\*.cs XKeys\*.cs ZInt\*.cs
diff --git a/build.sh b/build.sh
new file mode 100755
index 0000000..f54ab99
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,22 @@
+#! /bin/sh
+
+CSC=$(which mono-csc || which dmcs || which mcs || echo "none")
+
+if [ $CSC = "none" ]; then
+	echo "Error: please install mono-devel."
+	exit 1
+fi
+
+set -e
+
+echo "TestCrypto..."
+$CSC /out:TestCrypto.exe /main:TestCrypto Tests/*.cs Asn1/*.cs Crypto/*.cs SSLTLS/*.cs X500/*.cs XKeys/*.cs ZInt/*.cs
+
+#echo "Client..."
+#$CSC /out:Client.exe /main:Client Asn1/*.cs Crypto/*.cs SSLTLS/*.cs X500/*.cs XKeys/*.cs Client.cs
+
+#echo "Server..."
+#$CSC /out:Server.exe /main:Server Asn1/*.cs Crypto/*.cs SSLTLS/*.cs X500/*.cs XKeys/*.cs Server.cs
+
+echo "Twrch..."
+$CSC /out:Twrch.exe /main:Twrch Asn1/*.cs Crypto/*.cs SSLTLS/*.cs X500/*.cs XKeys/*.cs Twrch/*.cs
diff --git a/conf/bearssl.json b/conf/bearssl.json
new file mode 100644
index 0000000..d46f349
--- /dev/null
+++ b/conf/bearssl.json
@@ -0,0 +1,250 @@
+{
+  "commandFile" : "../build/brssl",
+  "commandArgs" : "twrch {0}",
+  "chainRSA" : "conf/rsacert.pem",
+  "skeyRSA" : "conf/rsakey.pem",
+  "chainEC" : "conf/eccert.pem",
+  "skeyEC" : "conf/eckey.pem",
+  "noCloseNotify" : false,
+  "versions" : [
+    "TLS10", "TLS11", "TLS12"
+  ],
+  "cipherSuites" : [
+    "ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256",
+    "ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256",
+    "ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
+    "ECDHE_RSA_WITH_AES_128_GCM_SHA256",
+    "ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
+    "ECDHE_RSA_WITH_AES_256_GCM_SHA384",
+    "ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
+    "ECDHE_RSA_WITH_AES_128_CBC_SHA256",
+    "ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
+    "ECDHE_RSA_WITH_AES_256_CBC_SHA384",
+    "ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
+    "ECDHE_RSA_WITH_AES_128_CBC_SHA",
+    "ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
+    "ECDHE_RSA_WITH_AES_256_CBC_SHA",
+
+    "ECDH_ECDSA_WITH_AES_128_GCM_SHA256",
+    "ECDH_ECDSA_WITH_AES_256_GCM_SHA384",
+    "ECDH_ECDSA_WITH_AES_128_CBC_SHA256",
+    "ECDH_ECDSA_WITH_AES_256_CBC_SHA384",
+    "ECDH_ECDSA_WITH_AES_128_CBC_SHA",
+    "ECDH_ECDSA_WITH_AES_256_CBC_SHA",
+
+    "RSA_WITH_AES_128_GCM_SHA256",
+    "RSA_WITH_AES_256_GCM_SHA384",
+    "RSA_WITH_AES_128_CBC_SHA256",
+    "RSA_WITH_AES_256_CBC_SHA256",
+    "RSA_WITH_AES_128_CBC_SHA",
+    "RSA_WITH_AES_256_CBC_SHA",
+    "RSA_WITH_3DES_EDE_CBC_SHA",
+
+    "ECDHE_ECDSA_WITH_3DES_EDE_CBC_SHA",
+    "ECDHE_RSA_WITH_3DES_EDE_CBC_SHA",
+    "ECDH_ECDSA_WITH_3DES_EDE_CBC_SHA"
+  ],
+  "hashAndSigns" : [
+    "RSA_SHA256",
+    "RSA_SHA224",
+    "RSA_SHA384",
+    "RSA_SHA512",
+    "RSA_SHA1",
+    "ECDSA_SHA256",
+    "ECDSA_SHA224",
+    "ECDSA_SHA384",
+    "ECDSA_SHA512",
+    "ECDSA_SHA1"
+  ],
+  "curves" : [
+    "Curve25519",
+    "NIST_P256",
+    "NIST_P384",
+    "NIST_P521"
+  ],
+  "tests" : [
+    {
+      "name" : "peerClose",
+      "comment" : "The peer should initiate a clean close",
+      "askClose" : "true"
+    },
+    {
+      "name" : "renegotiateNormal",
+      "comment" : "Normal renegotiation triggered from our side",
+      "renegotiate" : "true"
+    },
+    {
+      "name" : "peerRenegotiateNormal",
+      "comment" : "Normal renegotiation triggered by the peer",
+      "askRenegotiate" : "true"
+    },
+    {
+      "name" : "noSecureReneg",
+      "comment" : "Not sending secure renegotiation; renegotiation attempts should be rejected by the peer.",
+      "renegotiate" : "false",
+      "quirks" : {
+        "noSecureReneg" : "true"
+      }
+    },
+    {
+      "name" : "forceEmptySecureReneg",
+      "comment" : "Forcing empty Secure Renegotiation extension. This should be OK for first handshake, then fail during renegotiation.",
+      "renegotiate" : "true",
+      "expectedExitCode" : 1,
+      "expectedFailure" : "Unexpected transport closure",
+      "quirks" : {
+        "forceEmptySecureReneg" : "true"
+      }
+    },
+    {
+      "name" : "forceNonEmptySecureReneg",
+      "comment" : "A non-empty Secure Renegotiation extension is sent during the first handshake. The peer should call foul play and abort.",
+      "expectedExitCode" : 1,
+      "expectedFailure" : "Unexpected transport closure",
+      "quirks" : {
+        "forceNonEmptySecureReneg" : "true"
+      }
+    },
+    {
+      "name" : "alterNonEmptySecureReneg",
+      "comment" : "The Secure Renegotiation extension contents are altered during second handshake (but the length is preserved). The peer should abort.",
+      "renegotiate" : "true",
+      "expectedExitCode" : 1,
+      "expectedFailure" : "Unexpected transport closure",
+      "quirks" : {
+        "alterNonEmptySecureReneg" : "true"
+      }
+    },
+    {
+      "name" : "oversizedSecureReneg",
+      "comment" : "The Secure Renegotiation extension contents are much bigger than normal. The peer should abort.",
+      "expectedExitCode" : 1,
+      "expectedFailure" : "Unexpected transport closure",
+      "quirks" : {
+        "oversizedSecureReneg" : "true"
+      }
+    },
+    {
+      "name" : "recordSplitHalf",
+      "comment" : "All records of length 2 or more are split into two halves.",
+      "quirks" : {
+        "recordSplitMode" : "half:20,21,22,23"
+      }
+    },
+    {
+      "name" : "recordSplitZeroBefore",
+      "comment" : "All records are preceded with a zero-length record.",
+      "quirks" : {
+        "recordSplitMode" : "zero_before:20,21,22,23"
+      }
+    },
+    {
+      "name" : "recordSplitZeroHalf",
+      "comment" : "All records of length 2 or more are split into two halves, and a zero-length record is inserted between the two halves..",
+      "quirks" : {
+        "recordSplitMode" : "zero_half:20,21,22,23"
+      }
+    },
+    {
+      "name" : "recordSplitOneStart",
+      "comment" : "The first byte of each record of length 2 or more is separated into its own record.",
+      "quirks" : {
+        "recordSplitMode" : "one_start:20,21,22,23"
+      }
+    },
+    {
+      "name" : "recordSplitOneEnd",
+      "comment" : "The last byte of each record of length 2 or more is separated into its own record.",
+      "quirks" : {
+        "recordSplitMode" : "one_end:20,21,22,23"
+      }
+    },
+    {
+      "name" : "recordSplitMultiOne",
+      "comment" : "All records are split into individual records of length 1.",
+      "quirks" : {
+        "recordSplitMode" : "multi_one:20,21,22,23"
+      }
+    },
+    {
+      "name" : "emptyHandshake1",
+      "comment" : "An extra empty handshake message is inserted before the first application data record.",
+      "quirks" : {
+        "thresholdZeroHandshake" : 1
+      }
+    },
+    {
+      "name" : "emptyHandshake2",
+      "comment" : "An extra empty handshake message is inserted before the second application data record.",
+      "quirks" : {
+        "thresholdZeroHandshake" : 2
+      }
+    },
+    {
+      "name" : "emptyAppData1",
+      "comment" : "An extra empty handshake message is inserted before the first handshake record.",
+      "quirks" : {
+        "thresholdZeroAppData" : 1
+      }
+    },
+    {
+      "name" : "emptyAppData2",
+      "comment" : "An extra empty handshake message is inserted before the second handshake record.",
+      "quirks" : {
+        "thresholdZeroAppData" : 2
+      }
+    },
+    {
+      "name" : "extraServerExtension",
+      "comment" : "An extra extension is added in the ServerHello. Client should reject it. BearSSL closes the connection, so the server gets an unexpected transport closure.",
+      "clientOnly" : "true",
+      "expectedExitCode" : 1,
+      "expectedFailure" : "Unexpected transport closure",
+      "quirks" : {
+        "sendExtraExtension" : "0xA7C0"
+      }
+    },
+    {
+      "name" : "extraClientExtension",
+      "comment" : "An extra extension is added in the ClientHello. Server should ignore it.",
+      "serverOnly" : "true",
+      "quirks" : {
+        "sendExtraExtension" : "0xA7C0"
+      }
+    },
+    {
+      "name" : "reconnectSelf",
+      "comment" : "Connection is closed and reconnection is performed; the session should be resumed.",
+      "reconnect" : "self"
+    },
+    {
+      "name" : "reconnectPeer",
+      "comment" : "Peer is tasked with closing then reconnecting; the session should be resumed.",
+      "reconnect" : "peer"
+    },
+    {
+      "name" : "reconnectSelfForgetSelf",
+      "comment" : "Connection is closed and reconnection is performed. Previous session if forgotten on our part.",
+      "reconnect" : "self",
+      "forget" : "self"
+    },
+    {
+      "name" : "reconnectSelfForgetPeer",
+      "comment" : "Peer should forget session. Then we close and reconnect.",
+      "reconnect" : "self",
+      "forget" : "peer"
+    },
+    {
+      "name" : "reconnectPeerForgetSelf",
+      "comment" : "We forget the session. Peer should close and reconnect.",
+      "reconnect" : "peer",
+      "forget" : "self"
+    },
+    {
+      "name" : "reconnectPeerForgetPeer",
+      "comment" : "Peer should forget session. Peer should close and reconnect.",
+      "reconnect" : "peer",
+      "forget" : "peer"
+    }
+  ]
+}
diff --git a/conf/eccert.pem b/conf/eccert.pem
new file mode 100644
index 0000000..6de3d2f
--- /dev/null
+++ b/conf/eccert.pem
@@ -0,0 +1,9 @@
+-----BEGIN CERTIFICATE-----
+MIIBcDCCARWgAwIBAgIJANd5T8SS4w7GMAoGCCqGSM49BAMCMBQxEjAQBgNVBAMMCWxvY2FsaG9z
+dDAeFw0xNzAxMDEwMDAwMDBaFw0zNzEyMzEyMzU5NTlaMBQxEjAQBgNVBAMMCWxvY2FsaG9zdDBZ
+MBMGByqGSM49AgEGCCqGSM49AwEHA0IABDQAwvHS1NehjIe51KN7knKtaMhpFRR6WznOQlAVLAfY
+0Ciavy27K/uuQq4EakLKOEeiCj2BM2rcBxk8H49XX9KjUDBOMB0GA1UdDgQWBBSYWUJGpqkjBehD
+G9nG+aPmEIdbezAfBgNVHSMEGDAWgBSYWUJGpqkjBehDG9nG+aPmEIdbezAMBgNVHRMEBTADAQH/
+MAoGCCqGSM49BAMCA0kAMEYCIQCN1Jy39L/q84BGMi4CbhUxZ/0nkDzNmV5qVtXW9AOjuwIhANNA
+HpmN/KwgWM81fNjgdOOxZx+W1ksZsLqeEyCv0MK7
+-----END CERTIFICATE-----
diff --git a/conf/eckey.pem b/conf/eckey.pem
new file mode 100644
index 0000000..524b29c
--- /dev/null
+++ b/conf/eckey.pem
@@ -0,0 +1,5 @@
+-----BEGIN EC PRIVATE KEY-----
+MHcCAQEEIMCRS2RnKIEZdRwPuX+cYCsRestfVps6LEkpPy75xRp7oAoGCCqGSM49
+AwEHoUQDQgAENADC8dLU16GMh7nUo3uScq1oyGkVFHpbOc5CUBUsB9jQKJq/Lbsr
++65CrgRqQso4R6IKPYEzatwHGTwfj1df0g==
+-----END EC PRIVATE KEY-----
diff --git a/conf/rsacert.pem b/conf/rsacert.pem
new file mode 100644
index 0000000..b18b3f4
--- /dev/null
+++ b/conf/rsacert.pem
@@ -0,0 +1,16 @@
+-----BEGIN CERTIFICATE-----
+MIIC+zCCAeOgAwIBAgIJAKVPxGMRfdQpMA0GCSqGSIb3DQEBCwUAMBQxEjAQBgNVBAMMCWxvY2Fs
+aG9zdDAeFw0xNzAxMDEwMDAwMDBaFw0zNzEyMzEyMzU5NTlaMBQxEjAQBgNVBAMMCWxvY2FsaG9z
+dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKgYMcLD/5p+4NpY2BYxH+IiyQ4s9u5y
+fs3FN7bZAqBwiQ5WbDAyCuESqrITtcVvBMmh3J/+yomWp59cVr8HNEGe+OW0NugZtpAre0tJxxAW
+WnZ50UO/V20F/5Y4esy40mSGSOyje8nl00TWk7tlIsBGgxiT9QCFT+XKSmRTwc7kZBoPCeKGzxG0
+hPHU/Vc/tAJJza03VlG0z6FoDC1kHY57rq43jjzyOkKmm6FjrRJdtBfMLgVEcyHgmolIO155cGm7
+g2llp/Ir2UPOP4ktEPLqJnNpLLFZRhb2b6iGzCItEyuzd66EQjz2ddYfE5ZL/FbWPsuXGGFPZAT3
+dvU92lsCAwEAAaNQME4wHQYDVR0OBBYEFEfHRJNlQtw3IIUnNrmyId8RtrWkMB8GA1UdIwQYMBaA
+FEfHRJNlQtw3IIUnNrmyId8RtrWkMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBABIk
+UeU6lHgIwwb5qibE9CsIKnjlVR5nLodFQzYBNouB1v81fba/USJfMlHGiqcksL3h9ibzNS/a8PU2
+uSJKKR7Vgz/FN2PgqT4hzXEutKp3VP00+RnLmZDqAVaoSL+3cdMV1dNhwblfGTJEDX6QywUX2hdi
+SJ9muzUCP5oGHVvXjPKjDLYiti2amRJVui+B2cPHjb4l5+gmFwTVSQBuZZr8vFMFNVCIfMX5bR8T
+SRpGc+q1I7bCv3qbSy5xfjbRWHjNvYQ4Vr69PwL48t8To06GIbdvPLuxC1doFKxnUQMuqT+JPuD5
+wcvpmEj2eBvCkhrUP8Hz5z6ptiKrssfletk=
+-----END CERTIFICATE-----
diff --git a/conf/rsakey.pem b/conf/rsakey.pem
new file mode 100644
index 0000000..8d6301c
--- /dev/null
+++ b/conf/rsakey.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCoGDHCw/+afuDa
+WNgWMR/iIskOLPbucn7NxTe22QKgcIkOVmwwMgrhEqqyE7XFbwTJodyf/sqJlqef
+XFa/BzRBnvjltDboGbaQK3tLSccQFlp2edFDv1dtBf+WOHrMuNJkhkjso3vJ5dNE
+1pO7ZSLARoMYk/UAhU/lykpkU8HO5GQaDwnihs8RtITx1P1XP7QCSc2tN1ZRtM+h
+aAwtZB2Oe66uN4488jpCppuhY60SXbQXzC4FRHMh4JqJSDteeXBpu4NpZafyK9lD
+zj+JLRDy6iZzaSyxWUYW9m+ohswiLRMrs3euhEI89nXWHxOWS/xW1j7LlxhhT2QE
+93b1PdpbAgMBAAECggEBAJtjWkSoeNWx6lwN+xtwp/+clm2jRVWhw/SmFl3R+Cqm
+PRxi6boX2JS9c8wQil0Lxso59cB1gXd1LFkVvB71IupycbWuRX+DnY9ikqRDfGAz
+ucaBz+AntkLTY7TTWzl6tQs2U51ld15pNUcScRivYlOKG1ASHk8v7W8H9IMQJj4A
+xbKmhG8azL3Iyyb9rjiQIDwJziqMEJlJxCddKbGAGwVaCItSkxMKQCN016iaf05j
+uNLgOo9Jv6X7844Luw0k+uSRmRFEMoI8jatucJtmpZ2ElMJ54VBIUZG8tA/Pknm6
+8lrGyFAmWczRodN+OknfoXHrCRx/u7Hfct05qWsKR0ECgYEA0oPoOKQ+E2O1ULFP
+j2sk1iz6zWx6zqz0bjMs4IJDhFD3jfHHwocYNydHJ18xVbW4pjKhuHo8y1FjjIwo
+mKWa7/BphFp57CcSVlI8Ef2mlfVj7z1QnldpMgWvW6dQSRDOrbNBC6c7aDouJZYo
+D7sofCgs//rUSMY+aCthGlq+quECgYEAzGnlW60BuPF4GVk3Y9Sb7h0io8LjbzWo
+oXxscfVqFGgIsa+DqJ003KjLq+Uwu0+IVg2o3cp2oxzIlcmBrpen3mpRO6dxanT/
+l45kLG4dL8lXZzjw95xLSNdDXEcBLwCYc5521DPg+240PKwOQ+lcxwjyDoO3VEQg
+yeaBzSqgCLsCgYBRCvMFi0VSlZoh3IDyh58AzQQovVBx7GeVXSIztDJl5/3FqYTr
+wLJz2S0tXRpTEshpQyi7KmPpKgYW/4ZJbce+A2G70FELtub6UGJL0silBnlYitRU
+gPZAiau+ryTbXBsVB+NMpy7ZqzxEwA/gLn8hfR4F1fyPn7I6zChvyuuIQQKBgQCd
+fMjUhMpa7s8U2IOwSlGIdrIFcVVAjRrKr83tTqLX7f8kxpCtC9F6YCHq4b1V0sS7
+Z/K+TgpxSO/RV1quZPFUjpzfVPYwisuQvIe5I20hMAJC6L/eRXBLQm4HXj0vNUo/
+acsrWnzvucxNIlIrSFPOlLnJLPnF1mdcpldC9qAtmQKBgGr1ish0HfAu4B8g29c2
+1p0CPByClSuoqVPVBIQDwHYU05+ehO/wBbHu+/SzIUT7MrWrgYiyXbWosTE9QRsP
+vk9+/MgffqwfBexIxZ39yfeYo2xwnVNzRSzcF3pbxs5Eahtyhu9xDX/wX4jrpYEY
+J4sdGBnEqRp44V6WYtT32+gk
+-----END PRIVATE KEY-----
-- 
2.17.1