diff --git a/include/cryb/mpi.h b/include/cryb/mpi.h
index de147ff..d45d3fd 100644
--- a/include/cryb/mpi.h
+++ b/include/cryb/mpi.h
@@ -65,6 +65,8 @@ const char *cryb_mpi_version(void);
 #define mpi_lsb			cryb_mpi_lsb
 #define mpi_lshift		cryb_mpi_lshift
 #define mpi_msb			cryb_mpi_msb
+#define mpi_mul			cryb_mpi_mul
+#define mpi_mul_abs		cryb_mpi_mul_abs
 #define mpi_negate		cryb_mpi_negate
 #define mpi_rshift		cryb_mpi_rshift
 #define mpi_set			cryb_mpi_set
@@ -113,6 +115,8 @@ int mpi_load(cryb_mpi *, const uint8_t *, size_t);
 unsigned int mpi_lsb(const cryb_mpi *);
 int mpi_lshift(cryb_mpi *, unsigned int);
 unsigned int mpi_msb(const cryb_mpi *);
+int mpi_mul(cryb_mpi *, const cryb_mpi *, const cryb_mpi *);
+int mpi_mul_abs(cryb_mpi *, const cryb_mpi *, const cryb_mpi *);
 void mpi_negate(cryb_mpi *);
 int mpi_rshift(cryb_mpi *, unsigned int);
 int mpi_set(cryb_mpi *, int32_t);
diff --git a/lib/mpi/Makefile.am b/lib/mpi/Makefile.am
index 0458d7a..df4c455 100644
--- a/lib/mpi/Makefile.am
+++ b/lib/mpi/Makefile.am
@@ -22,6 +22,8 @@ libcryb_mpi_la_SOURCES = \
 	cryb_mpi_lsb.c \
 	cryb_mpi_lshift.c \
 	cryb_mpi_msb.c \
+	cryb_mpi_mul.c \
+	cryb_mpi_mul_abs.c \
 	cryb_mpi_negate.c \
 	cryb_mpi_rshift.c \
 	cryb_mpi_set.c \
diff --git a/lib/mpi/cryb_mpi_mul.c b/lib/mpi/cryb_mpi_mul.c
new file mode 100644
index 0000000..b0363e2
--- /dev/null
+++ b/lib/mpi/cryb_mpi_mul.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright (c) 2017 Dag-Erling Smørgrav
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote
+ *    products derived from this software without specific prior written
+ *    permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "cryb/impl.h"
+
+#include <stddef.h>
+#include <stdint.h>
+
+#include <cryb/mpi.h>
+
+#include "cryb_mpi_impl.h"
+
+/*
+ * Store the product of A and B in X.
+ */
+int
+mpi_mul(cryb_mpi *X, const cryb_mpi *A, const cryb_mpi *B)
+{
+	int neg;
+
+	neg = A->neg ^ B->neg;
+	if (mpi_mul_abs(X, A, B) < 0)
+		return (-1);
+	if (X->msb > 0)
+		X->neg = neg;
+	return (0);
+}
+
diff --git a/lib/mpi/cryb_mpi_mul_abs.c b/lib/mpi/cryb_mpi_mul_abs.c
new file mode 100644
index 0000000..37f5ad6
--- /dev/null
+++ b/lib/mpi/cryb_mpi_mul_abs.c
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2017 Dag-Erling Smørgrav
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ * 3. The name of the author may not be used to endorse or promote
+ *    products derived from this software without specific prior written
+ *    permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include "cryb/impl.h"
+
+#include <stddef.h>
+#include <stdint.h>
+#include <strings.h>
+
+#include <cryb/bitwise.h>
+#include <cryb/mpi.h>
+
+#include "cryb_mpi_impl.h"
+
+/*
+ * Store the product of the absolutes values of A and B in X.
+ *
+ * Note: this is loosely based on HAC 14.2.3.  However, the first
+ * paragraph speaks of x and y having n and t digits respectively, with
+ * the most significant digits being x[n - 1] and y[t - 1], but the rest
+ * of the section refers to x and y having n + 1 and t + 1 digits.  We
+ * follow the former convention, but use a and b instead of n and t to
+ * distance ourselves from this confusing text.
+ */
+int
+mpi_mul_abs(cryb_mpi *X, const cryb_mpi *A, const cryb_mpi *B)
+{
+	cryb_mpi *P, T = CRYB_MPI_ZERO;
+	uint64_t p;
+	unsigned int a, b, i, j;
+	uint32_t c;
+
+	/*
+	 * Trivial cases: A and / or B is zero.
+	 */
+	if (A->msb == 0 || B->msb == 0) {
+		mpi_zero(X);
+		return (0);
+	}
+
+	/*
+	 * Unlike addition or subtraction, we can't multiply in place, so
+	 * if the destination is identical with one of the operands, we
+	 * will have to use temporary storage.  Either way, make sure we
+	 * have enough room for the product.
+	 */
+	P = (X == A || X == B) ? &T : X;
+	mpi_zero(P);
+	a = (A->msb + 31) / 32;
+	b = (B->msb + 31) / 32;
+	if (mpi_grow(P, (a + b) * 32) != 0)
+		return (-1);
+
+	/*
+	 * Multiply...
+	 */
+	for (i = 0; i < b; ++i) {
+		for (c = j = 0; j < a; ++j) {
+			p = 1ULL * A->words[j] * B->words[i] + c;
+			P->words[i + j] += p & 0xffffffffUL;
+			c = p >> 32;
+		}
+		P->words[i + j] = c;
+	}
+	if (P->words[i = a + b - 1] == 0)
+		--i;
+	P->msb = i * 32 + flsl(P->words[i]);
+
+	/*
+	 * If we used temporary storage, copy back and clean up.
+	 */
+	if (P == &T) {
+		cryb_mpi_swap(X, P);
+		cryb_mpi_destroy(P);
+	}
+
+	return (0);
+}
diff --git a/t/t_mpi_muldiv.c b/t/t_mpi_muldiv.c
index e4c6be9..dcbad57 100644
--- a/t/t_mpi_muldiv.c
+++ b/t/t_mpi_muldiv.c
@@ -59,12 +59,54 @@ static struct t_mul_case {
 	size_t emsb;
 	int eneg:1;
 } t_mul_cases[] = {
+	{
+		"0 * 0 == 0",
+		{                                                      },  0, 0,
+		{                                                      },  0, 0,
+		{                                                      },  0, 0,
+	},
+	{
+		"0 * 1 == 0",
+		{                                                      },  0, 0,
+		{                                                 0x01 },  1, 0,
+		{                                                      },  0, 0,
+	},
+	{
+		"1 * 0 == 0",
+		{                                                 0x01 },  1, 0,
+		{                                                      },  0, 0,
+		{                                                      },  0, 0,
+	},
 	{
 		"1 * 1 == 1",
 		{                                                 0x01 },  1, 0,
 		{                                                 0x01 },  1, 0,
 		{                                                 0x01 },  1, 0,
 	},
+	{
+		"(2^32 + 1) * (2^32 + 1) == 2^64 + 2^33 + 1",
+		{                         0x01, 0x00, 0x00, 0x00, 0x01 }, 33, 0,
+		{                         0x01, 0x00, 0x00, 0x00, 0x01 }, 33, 0,
+		{ 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01 }, 65, 0,
+	},
+	{
+		"(2^32 + 1) * (2^32 - 1) == 2^64 - 1",
+		{                         0x01, 0x00, 0x00, 0x00, 0x01 }, 33, 0,
+		{                               0xff, 0xff, 0xff, 0xff }, 32, 0,
+		{       0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, 63, 0,
+	},
+	{
+		"(2^32 - 1) * (2^32 + 1) == 2^64 - 1",
+		{                               0xff, 0xff, 0xff, 0xff }, 32, 0,
+		{                         0x01, 0x00, 0x00, 0x00, 0x01 }, 33, 0,
+		{       0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff }, 63, 0,
+	},
+	{
+		"(2^32 - 1) * (2^32 - 1) == 2^64 - 2^33 + 1",
+		{                               0xff, 0xff, 0xff, 0xff }, 32, 0,
+		{                               0xff, 0xff, 0xff, 0xff }, 32, 0,
+		{       0xff, 0xff, 0xff, 0xfe, 0x00, 0x00, 0x00, 0x01 }, 64, 0,
+	},
 };
 
 static int
@@ -81,11 +123,7 @@ t_mpi_mul_tc(char **desc CRYB_UNUSED, void *arg)
 	b.neg = tc->bneg;
 	mpi_load(&e, tc->e, (tc->emsb + 7) / 8);
 	e.neg = tc->eneg;
-#if 0
 	ret &= t_compare_i(0, mpi_mul_abs(&x, &a, &b));
-#else
-	mpi_load(&x, tc->e, (tc->emsb + 7) / 8);
-#endif
 	ret &= t_compare_mpi(&e, &x);
 	mpi_destroy(&a);
 	mpi_destroy(&b);