lkundrak / rpms / hostapd

Forked from rpms/hostapd 4 years ago
Clone
John W. Linville aeb7fa6
From aaf65feac67c3993935634eefe5bc76b9fce03aa Mon Sep 17 00:00:00 2001
John W. Linville aeb7fa6
From: Jouni Malinen <jouni@codeaurora.org>
John W. Linville aeb7fa6
Date: Tue, 26 Feb 2019 11:59:45 +0200
John W. Linville aeb7fa6
Subject: [PATCH 04/14] EAP-pwd: Use constant time and memory access for
John W. Linville aeb7fa6
 finding the PWE
John W. Linville aeb7fa6
John W. Linville aeb7fa6
This algorithm could leak information to external observers in form of
John W. Linville aeb7fa6
timing differences or memory access patterns (cache use). While the
John W. Linville aeb7fa6
previous implementation had protection against the most visible timing
John W. Linville aeb7fa6
differences (looping 40 rounds and masking the legendre operation), it
John W. Linville aeb7fa6
did not protect against memory access patterns between the two possible
John W. Linville aeb7fa6
code paths in the masking operations. That might be sufficient to allow
John W. Linville aeb7fa6
an unprivileged process running on the same device to be able to
John W. Linville aeb7fa6
determine which path is being executed through a cache attack and based
John W. Linville aeb7fa6
on that, determine information about the used password.
John W. Linville aeb7fa6
John W. Linville aeb7fa6
Convert the PWE finding loop to use constant time functions and
John W. Linville aeb7fa6
identical memory access path without different branches for the QR/QNR
John W. Linville aeb7fa6
cases to minimize possible side-channel information similarly to the
John W. Linville aeb7fa6
changes done for SAE authentication. (CVE-2019-9495)
John W. Linville aeb7fa6
John W. Linville aeb7fa6
Signed-off-by: Jouni Malinen <jouni@codeaurora.org>
John W. Linville aeb7fa6
---
John W. Linville aeb7fa6
 src/eap_common/eap_pwd_common.c | 187 +++++++++++++++++++++-------------------
John W. Linville aeb7fa6
 1 file changed, 99 insertions(+), 88 deletions(-)
John W. Linville aeb7fa6
John W. Linville aeb7fa6
diff --git a/src/eap_common/eap_pwd_common.c b/src/eap_common/eap_pwd_common.c
John W. Linville aeb7fa6
index 02fe01e..e49aaf8 100644
John W. Linville aeb7fa6
--- a/src/eap_common/eap_pwd_common.c
John W. Linville aeb7fa6
+++ b/src/eap_common/eap_pwd_common.c
John W. Linville aeb7fa6
@@ -8,11 +8,15 @@
John W. Linville aeb7fa6
 
John W. Linville aeb7fa6
 #include "includes.h"
John W. Linville aeb7fa6
 #include "common.h"
John W. Linville aeb7fa6
+#include "utils/const_time.h"
John W. Linville aeb7fa6
 #include "crypto/sha256.h"
John W. Linville aeb7fa6
 #include "crypto/crypto.h"
John W. Linville aeb7fa6
 #include "eap_defs.h"
John W. Linville aeb7fa6
 #include "eap_pwd_common.h"
John W. Linville aeb7fa6
 
John W. Linville aeb7fa6
+#define MAX_ECC_PRIME_LEN 66
John W. Linville aeb7fa6
+
John W. Linville aeb7fa6
+
John W. Linville aeb7fa6
 /* The random function H(x) = HMAC-SHA256(0^32, x) */
John W. Linville aeb7fa6
 struct crypto_hash * eap_pwd_h_init(void)
John W. Linville aeb7fa6
 {
John W. Linville aeb7fa6
@@ -102,6 +106,15 @@ EAP_PWD_group * get_eap_pwd_group(u16 num)
John W. Linville aeb7fa6
 }
John W. Linville aeb7fa6
 
John W. Linville aeb7fa6
 
John W. Linville aeb7fa6
+static void buf_shift_right(u8 *buf, size_t len, size_t bits)
John W. Linville aeb7fa6
+{
John W. Linville aeb7fa6
+	size_t i;
John W. Linville aeb7fa6
+	for (i = len - 1; i > 0; i--)
John W. Linville aeb7fa6
+		buf[i] = (buf[i - 1] << (8 - bits)) | (buf[i] >> bits);
John W. Linville aeb7fa6
+	buf[0] >>= bits;
John W. Linville aeb7fa6
+}
John W. Linville aeb7fa6
+
John W. Linville aeb7fa6
+
John W. Linville aeb7fa6
 /*
John W. Linville aeb7fa6
  * compute a "random" secret point on an elliptic curve based
John W. Linville aeb7fa6
  * on the password and identities.
John W. Linville aeb7fa6
@@ -113,17 +126,27 @@ int compute_password_element(EAP_PWD_group *grp, u16 num,
John W. Linville aeb7fa6
 			     const u8 *token)
John W. Linville aeb7fa6
 {
John W. Linville aeb7fa6
 	struct crypto_bignum *qr = NULL, *qnr = NULL, *one = NULL;
John W. Linville aeb7fa6
+	struct crypto_bignum *qr_or_qnr = NULL;
John W. Linville aeb7fa6
+	u8 qr_bin[MAX_ECC_PRIME_LEN];
John W. Linville aeb7fa6
+	u8 qnr_bin[MAX_ECC_PRIME_LEN];
John W. Linville aeb7fa6
+	u8 qr_or_qnr_bin[MAX_ECC_PRIME_LEN];
John W. Linville aeb7fa6
+	u8 x_bin[MAX_ECC_PRIME_LEN];
John W. Linville aeb7fa6
 	struct crypto_bignum *tmp1 = NULL, *tmp2 = NULL, *pm1 = NULL;
John W. Linville aeb7fa6
 	struct crypto_hash *hash;
John W. Linville aeb7fa6
 	unsigned char pwe_digest[SHA256_MAC_LEN], *prfbuf = NULL, ctr;
John W. Linville aeb7fa6
-	int is_odd, ret = 0, check, found = 0;
John W. Linville aeb7fa6
-	size_t primebytelen, primebitlen;
John W. Linville aeb7fa6
-	struct crypto_bignum *x_candidate = NULL, *rnd = NULL, *cofactor = NULL;
John W. Linville aeb7fa6
+	int ret = 0, check, res;
John W. Linville aeb7fa6
+	u8 found = 0; /* 0 (false) or 0xff (true) to be used as const_time_*
John W. Linville aeb7fa6
+		       * mask */
John W. Linville aeb7fa6
+	size_t primebytelen = 0, primebitlen;
John W. Linville aeb7fa6
+	struct crypto_bignum *x_candidate = NULL, *cofactor = NULL;
John W. Linville aeb7fa6
 	const struct crypto_bignum *prime;
John W. Linville aeb7fa6
+	u8 mask, found_ctr = 0, is_odd = 0;
John W. Linville aeb7fa6
 
John W. Linville aeb7fa6
 	if (grp->pwe)
John W. Linville aeb7fa6
 		return -1;
John W. Linville aeb7fa6
 
John W. Linville aeb7fa6
+	os_memset(x_bin, 0, sizeof(x_bin));
John W. Linville aeb7fa6
+
John W. Linville aeb7fa6
 	prime = crypto_ec_get_prime(grp->group);
John W. Linville aeb7fa6
 	cofactor = crypto_bignum_init();
John W. Linville aeb7fa6
 	grp->pwe = crypto_ec_point_init(grp->group);
John W. Linville aeb7fa6
@@ -152,8 +175,6 @@ int compute_password_element(EAP_PWD_group *grp, u16 num,
John W. Linville aeb7fa6
 
John W. Linville aeb7fa6
 	/* get a random quadratic residue and nonresidue */
John W. Linville aeb7fa6
 	while (!qr || !qnr) {
John W. Linville aeb7fa6
-		int res;
John W. Linville aeb7fa6
-
John W. Linville aeb7fa6
 		if (crypto_bignum_rand(tmp1, prime) < 0)
John W. Linville aeb7fa6
 			goto fail;
John W. Linville aeb7fa6
 		res = crypto_bignum_legendre(tmp1, prime);
John W. Linville aeb7fa6
@@ -167,6 +188,11 @@ int compute_password_element(EAP_PWD_group *grp, u16 num,
John W. Linville aeb7fa6
 		if (!tmp1)
John W. Linville aeb7fa6
 			goto fail;
John W. Linville aeb7fa6
 	}
John W. Linville aeb7fa6
+	if (crypto_bignum_to_bin(qr, qr_bin, sizeof(qr_bin),
John W. Linville aeb7fa6
+				 primebytelen) < 0 ||
John W. Linville aeb7fa6
+	    crypto_bignum_to_bin(qnr, qnr_bin, sizeof(qnr_bin),
John W. Linville aeb7fa6
+				 primebytelen) < 0)
John W. Linville aeb7fa6
+		goto fail;
John W. Linville aeb7fa6
 
John W. Linville aeb7fa6
 	os_memset(prfbuf, 0, primebytelen);
John W. Linville aeb7fa6
 	ctr = 0;
John W. Linville aeb7fa6
@@ -194,17 +220,16 @@ int compute_password_element(EAP_PWD_group *grp, u16 num,
John W. Linville aeb7fa6
 		eap_pwd_h_update(hash, &ctr, sizeof(ctr));
John W. Linville aeb7fa6
 		eap_pwd_h_final(hash, pwe_digest);
John W. Linville aeb7fa6
 
John W. Linville aeb7fa6
-		crypto_bignum_deinit(rnd, 1);
John W. Linville aeb7fa6
-		rnd = crypto_bignum_init_set(pwe_digest, SHA256_MAC_LEN);
John W. Linville aeb7fa6
-		if (!rnd) {
John W. Linville aeb7fa6
-			wpa_printf(MSG_INFO, "EAP-pwd: unable to create rnd");
John W. Linville aeb7fa6
-			goto fail;
John W. Linville aeb7fa6
-		}
John W. Linville aeb7fa6
+		is_odd = const_time_select_u8(
John W. Linville aeb7fa6
+			found, is_odd, pwe_digest[SHA256_MAC_LEN - 1] & 0x01);
John W. Linville aeb7fa6
 		if (eap_pwd_kdf(pwe_digest, SHA256_MAC_LEN,
John W. Linville aeb7fa6
 				(u8 *) "EAP-pwd Hunting And Pecking",
John W. Linville aeb7fa6
 				os_strlen("EAP-pwd Hunting And Pecking"),
John W. Linville aeb7fa6
 				prfbuf, primebitlen) < 0)
John W. Linville aeb7fa6
 			goto fail;
John W. Linville aeb7fa6
+		if (primebitlen % 8)
John W. Linville aeb7fa6
+			buf_shift_right(prfbuf, primebytelen,
John W. Linville aeb7fa6
+					8 - primebitlen % 8);
John W. Linville aeb7fa6
 
John W. Linville aeb7fa6
 		crypto_bignum_deinit(x_candidate, 1);
John W. Linville aeb7fa6
 		x_candidate = crypto_bignum_init_set(prfbuf, primebytelen);
John W. Linville aeb7fa6
@@ -214,24 +239,13 @@ int compute_password_element(EAP_PWD_group *grp, u16 num,
John W. Linville aeb7fa6
 			goto fail;
John W. Linville aeb7fa6
 		}
John W. Linville aeb7fa6
 
John W. Linville aeb7fa6
-		/*
John W. Linville aeb7fa6
-		 * eap_pwd_kdf() returns a string of bits 0..primebitlen but
John W. Linville aeb7fa6
-		 * BN_bin2bn will treat that string of bits as a big endian
John W. Linville aeb7fa6
-		 * number. If the primebitlen is not an even multiple of 8
John W. Linville aeb7fa6
-		 * then excessive bits-- those _after_ primebitlen-- so now
John W. Linville aeb7fa6
-		 * we have to shift right the amount we masked off.
John W. Linville aeb7fa6
-		 */
John W. Linville aeb7fa6
-		if ((primebitlen % 8) &&
John W. Linville aeb7fa6
-		    crypto_bignum_rshift(x_candidate,
John W. Linville aeb7fa6
-					 (8 - (primebitlen % 8)),
John W. Linville aeb7fa6
-					 x_candidate) < 0)
John W. Linville aeb7fa6
-			goto fail;
John W. Linville aeb7fa6
-
John W. Linville aeb7fa6
 		if (crypto_bignum_cmp(x_candidate, prime) >= 0)
John W. Linville aeb7fa6
 			continue;
John W. Linville aeb7fa6
 
John W. Linville aeb7fa6
-		wpa_hexdump(MSG_DEBUG, "EAP-pwd: x_candidate",
John W. Linville aeb7fa6
-			    prfbuf, primebytelen);
John W. Linville aeb7fa6
+		wpa_hexdump_key(MSG_DEBUG, "EAP-pwd: x_candidate",
John W. Linville aeb7fa6
+				prfbuf, primebytelen);
John W. Linville aeb7fa6
+		const_time_select_bin(found, x_bin, prfbuf, primebytelen,
John W. Linville aeb7fa6
+				      x_bin);
John W. Linville aeb7fa6
 
John W. Linville aeb7fa6
 		/*
John W. Linville aeb7fa6
 		 * compute y^2 using the equation of the curve
John W. Linville aeb7fa6
@@ -261,13 +275,15 @@ int compute_password_element(EAP_PWD_group *grp, u16 num,
John W. Linville aeb7fa6
 		 * Flip a coin, multiply by the random quadratic residue or the
John W. Linville aeb7fa6
 		 * random quadratic nonresidue and record heads or tails.
John W. Linville aeb7fa6
 		 */
John W. Linville aeb7fa6
-		if (crypto_bignum_is_odd(tmp1)) {
John W. Linville aeb7fa6
-			crypto_bignum_mulmod(tmp2, qr, prime, tmp2);
John W. Linville aeb7fa6
-			check = 1;
John W. Linville aeb7fa6
-		} else {
John W. Linville aeb7fa6
-			crypto_bignum_mulmod(tmp2, qnr, prime, tmp2);
John W. Linville aeb7fa6
-			check = -1;
John W. Linville aeb7fa6
-		}
John W. Linville aeb7fa6
+		mask = const_time_eq_u8(crypto_bignum_is_odd(tmp1), 1);
John W. Linville aeb7fa6
+		check = const_time_select_s8(mask, 1, -1);
John W. Linville aeb7fa6
+		const_time_select_bin(mask, qr_bin, qnr_bin, primebytelen,
John W. Linville aeb7fa6
+				      qr_or_qnr_bin);
John W. Linville aeb7fa6
+		crypto_bignum_deinit(qr_or_qnr, 1);
John W. Linville aeb7fa6
+		qr_or_qnr = crypto_bignum_init_set(qr_or_qnr_bin, primebytelen);
John W. Linville aeb7fa6
+		if (!qr_or_qnr ||
John W. Linville aeb7fa6
+		    crypto_bignum_mulmod(tmp2, qr_or_qnr, prime, tmp2) < 0)
John W. Linville aeb7fa6
+			goto fail;
John W. Linville aeb7fa6
 
John W. Linville aeb7fa6
 		/*
John W. Linville aeb7fa6
 		 * Now it's safe to do legendre, if check is 1 then it's
John W. Linville aeb7fa6
@@ -275,59 +291,12 @@ int compute_password_element(EAP_PWD_group *grp, u16 num,
John W. Linville aeb7fa6
 		 * change result), if check is -1 then it's the opposite test
John W. Linville aeb7fa6
 		 * (multiplying a qr by qnr would make a qnr).
John W. Linville aeb7fa6
 		 */
John W. Linville aeb7fa6
-		if (crypto_bignum_legendre(tmp2, prime) == check) {
John W. Linville aeb7fa6
-			if (found == 1)
John W. Linville aeb7fa6
-				continue;
John W. Linville aeb7fa6
-
John W. Linville aeb7fa6
-			/* need to unambiguously identify the solution */
John W. Linville aeb7fa6
-			is_odd = crypto_bignum_is_odd(rnd);
John W. Linville aeb7fa6
-
John W. Linville aeb7fa6
-			/*
John W. Linville aeb7fa6
-			 * We know x_candidate is a quadratic residue so set
John W. Linville aeb7fa6
-			 * it here.
John W. Linville aeb7fa6
-			 */
John W. Linville aeb7fa6
-			if (crypto_ec_point_solve_y_coord(grp->group, grp->pwe,
John W. Linville aeb7fa6
-							  x_candidate,
John W. Linville aeb7fa6
-							  is_odd) != 0) {
John W. Linville aeb7fa6
-				wpa_printf(MSG_INFO,
John W. Linville aeb7fa6
-					   "EAP-pwd: Could not solve for y");
John W. Linville aeb7fa6
-				continue;
John W. Linville aeb7fa6
-			}
John W. Linville aeb7fa6
-
John W. Linville aeb7fa6
-			/*
John W. Linville aeb7fa6
-			 * If there's a solution to the equation then the point
John W. Linville aeb7fa6
-			 * must be on the curve so why check again explicitly?
John W. Linville aeb7fa6
-			 * OpenSSL code says this is required by X9.62. We're
John W. Linville aeb7fa6
-			 * not X9.62 but it can't hurt just to be sure.
John W. Linville aeb7fa6
-			 */
John W. Linville aeb7fa6
-			if (!crypto_ec_point_is_on_curve(grp->group,
John W. Linville aeb7fa6
-							 grp->pwe)) {
John W. Linville aeb7fa6
-				wpa_printf(MSG_INFO,
John W. Linville aeb7fa6
-					   "EAP-pwd: point is not on curve");
John W. Linville aeb7fa6
-				continue;
John W. Linville aeb7fa6
-			}
John W. Linville aeb7fa6
-
John W. Linville aeb7fa6
-			if (!crypto_bignum_is_one(cofactor)) {
John W. Linville aeb7fa6
-				/* make sure the point is not in a small
John W. Linville aeb7fa6
-				 * sub-group */
John W. Linville aeb7fa6
-				if (crypto_ec_point_mul(grp->group, grp->pwe,
John W. Linville aeb7fa6
-							cofactor,
John W. Linville aeb7fa6
-							grp->pwe) != 0) {
John W. Linville aeb7fa6
-					wpa_printf(MSG_INFO,
John W. Linville aeb7fa6
-						   "EAP-pwd: cannot multiply generator by order");
John W. Linville aeb7fa6
-					continue;
John W. Linville aeb7fa6
-				}
John W. Linville aeb7fa6
-				if (crypto_ec_point_is_at_infinity(grp->group,
John W. Linville aeb7fa6
-								   grp->pwe)) {
John W. Linville aeb7fa6
-					wpa_printf(MSG_INFO,
John W. Linville aeb7fa6
-						   "EAP-pwd: point is at infinity");
John W. Linville aeb7fa6
-					continue;
John W. Linville aeb7fa6
-				}
John W. Linville aeb7fa6
-			}
John W. Linville aeb7fa6
-			wpa_printf(MSG_DEBUG,
John W. Linville aeb7fa6
-				   "EAP-pwd: found a PWE in %d tries", ctr);
John W. Linville aeb7fa6
-			found = 1;
John W. Linville aeb7fa6
-		}
John W. Linville aeb7fa6
+		res = crypto_bignum_legendre(tmp2, prime);
John W. Linville aeb7fa6
+		if (res == -2)
John W. Linville aeb7fa6
+			goto fail;
John W. Linville aeb7fa6
+		mask = const_time_eq(res, check);
John W. Linville aeb7fa6
+		found_ctr = const_time_select_u8(found, found_ctr, ctr);
John W. Linville aeb7fa6
+		found |= mask;
John W. Linville aeb7fa6
 	}
John W. Linville aeb7fa6
 	if (found == 0) {
John W. Linville aeb7fa6
 		wpa_printf(MSG_INFO,
John W. Linville aeb7fa6
@@ -335,6 +304,44 @@ int compute_password_element(EAP_PWD_group *grp, u16 num,
John W. Linville aeb7fa6
 			   num);
John W. Linville aeb7fa6
 		goto fail;
John W. Linville aeb7fa6
 	}
John W. Linville aeb7fa6
+
John W. Linville aeb7fa6
+	/*
John W. Linville aeb7fa6
+	 * We know x_candidate is a quadratic residue so set it here.
John W. Linville aeb7fa6
+	 */
John W. Linville aeb7fa6
+	crypto_bignum_deinit(x_candidate, 1);
John W. Linville aeb7fa6
+	x_candidate = crypto_bignum_init_set(x_bin, primebytelen);
John W. Linville aeb7fa6
+	if (!x_candidate ||
John W. Linville aeb7fa6
+	    crypto_ec_point_solve_y_coord(grp->group, grp->pwe, x_candidate,
John W. Linville aeb7fa6
+					  is_odd) != 0) {
John W. Linville aeb7fa6
+		wpa_printf(MSG_INFO, "EAP-pwd: Could not solve for y");
John W. Linville aeb7fa6
+		goto fail;
John W. Linville aeb7fa6
+	}
John W. Linville aeb7fa6
+
John W. Linville aeb7fa6
+	/*
John W. Linville aeb7fa6
+	 * If there's a solution to the equation then the point must be on the
John W. Linville aeb7fa6
+	 * curve so why check again explicitly? OpenSSL code says this is
John W. Linville aeb7fa6
+	 * required by X9.62. We're not X9.62 but it can't hurt just to be sure.
John W. Linville aeb7fa6
+	 */
John W. Linville aeb7fa6
+	if (!crypto_ec_point_is_on_curve(grp->group, grp->pwe)) {
John W. Linville aeb7fa6
+		wpa_printf(MSG_INFO, "EAP-pwd: point is not on curve");
John W. Linville aeb7fa6
+		goto fail;
John W. Linville aeb7fa6
+	}
John W. Linville aeb7fa6
+
John W. Linville aeb7fa6
+	if (!crypto_bignum_is_one(cofactor)) {
John W. Linville aeb7fa6
+		/* make sure the point is not in a small sub-group */
John W. Linville aeb7fa6
+		if (crypto_ec_point_mul(grp->group, grp->pwe, cofactor,
John W. Linville aeb7fa6
+					grp->pwe) != 0) {
John W. Linville aeb7fa6
+			wpa_printf(MSG_INFO,
John W. Linville aeb7fa6
+				   "EAP-pwd: cannot multiply generator by order");
John W. Linville aeb7fa6
+			goto fail;
John W. Linville aeb7fa6
+		}
John W. Linville aeb7fa6
+		if (crypto_ec_point_is_at_infinity(grp->group, grp->pwe)) {
John W. Linville aeb7fa6
+			wpa_printf(MSG_INFO, "EAP-pwd: point is at infinity");
John W. Linville aeb7fa6
+			goto fail;
John W. Linville aeb7fa6
+		}
John W. Linville aeb7fa6
+	}
John W. Linville aeb7fa6
+	wpa_printf(MSG_DEBUG, "EAP-pwd: found a PWE in %02d tries", found_ctr);
John W. Linville aeb7fa6
+
John W. Linville aeb7fa6
 	if (0) {
John W. Linville aeb7fa6
  fail:
John W. Linville aeb7fa6
 		crypto_ec_point_deinit(grp->pwe, 1);
John W. Linville aeb7fa6
@@ -344,14 +351,18 @@ int compute_password_element(EAP_PWD_group *grp, u16 num,
John W. Linville aeb7fa6
 	/* cleanliness and order.... */
John W. Linville aeb7fa6
 	crypto_bignum_deinit(cofactor, 1);
John W. Linville aeb7fa6
 	crypto_bignum_deinit(x_candidate, 1);
John W. Linville aeb7fa6
-	crypto_bignum_deinit(rnd, 1);
John W. Linville aeb7fa6
 	crypto_bignum_deinit(pm1, 0);
John W. Linville aeb7fa6
 	crypto_bignum_deinit(tmp1, 1);
John W. Linville aeb7fa6
 	crypto_bignum_deinit(tmp2, 1);
John W. Linville aeb7fa6
 	crypto_bignum_deinit(qr, 1);
John W. Linville aeb7fa6
 	crypto_bignum_deinit(qnr, 1);
John W. Linville aeb7fa6
+	crypto_bignum_deinit(qr_or_qnr, 1);
John W. Linville aeb7fa6
 	crypto_bignum_deinit(one, 0);
John W. Linville aeb7fa6
-	os_free(prfbuf);
John W. Linville aeb7fa6
+	bin_clear_free(prfbuf, primebytelen);
John W. Linville aeb7fa6
+	os_memset(qr_bin, 0, sizeof(qr_bin));
John W. Linville aeb7fa6
+	os_memset(qnr_bin, 0, sizeof(qnr_bin));
John W. Linville aeb7fa6
+	os_memset(qr_or_qnr_bin, 0, sizeof(qr_or_qnr_bin));
John W. Linville aeb7fa6
+	os_memset(pwe_digest, 0, sizeof(pwe_digest));
John W. Linville aeb7fa6
 
John W. Linville aeb7fa6
 	return ret;
John W. Linville aeb7fa6
 }
John W. Linville aeb7fa6
-- 
John W. Linville aeb7fa6
2.7.4
John W. Linville aeb7fa6