use a time constant comparison function for security
For HMAC and password hash comparisons we need to use a time constant comparison that always compares the whole value in order to protect against timing attacks. Original commit: elastic/x-pack-elasticsearch@f6082c76b9
This commit is contained in:
parent
d1759ff322
commit
197817e900
|
@ -751,12 +751,12 @@ public class BCrypt {
|
||||||
* Check that a plaintext password matches a previously hashed
|
* Check that a plaintext password matches a previously hashed
|
||||||
* one.
|
* one.
|
||||||
*
|
*
|
||||||
* Modified from the original to take a SecuredString plaintext
|
* Modified from the original to take a SecuredString plaintext and use a constant time comparison
|
||||||
* @param plaintext the plaintext password to verify
|
* @param plaintext the plaintext password to verify
|
||||||
* @param hashed the previously-hashed password
|
* @param hashed the previously-hashed password
|
||||||
* @return true if the passwords match, false otherwise
|
* @return true if the passwords match, false otherwise
|
||||||
*/
|
*/
|
||||||
public static boolean checkpw(SecuredString plaintext, String hashed) {
|
public static boolean checkpw(SecuredString plaintext, String hashed) {
|
||||||
return hashed.compareTo(hashpw(plaintext, hashed)) == 0;
|
return SecuredString.constantTimeEquals(hashed, hashpw(plaintext, hashed));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ import org.elasticsearch.shield.ShieldSettingsException;
|
||||||
|
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.concurrent.ThreadLocalRandom;
|
import java.util.concurrent.ThreadLocalRandom;
|
||||||
|
@ -44,22 +43,23 @@ public enum Hasher {
|
||||||
}
|
}
|
||||||
if (hashStr.startsWith(PLAIN_PREFIX)) {
|
if (hashStr.startsWith(PLAIN_PREFIX)) {
|
||||||
hashStr = hashStr.substring(PLAIN_PREFIX.length());
|
hashStr = hashStr.substring(PLAIN_PREFIX.length());
|
||||||
return text.equals(hashStr);
|
return SecuredString.constantTimeEquals(text, hashStr);
|
||||||
}
|
}
|
||||||
byte[] textBytes = CharArrays.toUtf8Bytes(text.internalChars());
|
byte[] textBytes = CharArrays.toUtf8Bytes(text.internalChars());
|
||||||
if (hashStr.startsWith(APR1_PREFIX)) {
|
if (hashStr.startsWith(APR1_PREFIX)) {
|
||||||
return hashStr.compareTo(Md5Crypt.apr1Crypt(textBytes, hashStr)) == 0;
|
return SecuredString.constantTimeEquals(hashStr, Md5Crypt.apr1Crypt(textBytes, hashStr));
|
||||||
}
|
}
|
||||||
if (hashStr.startsWith(SHA1_PREFIX)) {
|
if (hashStr.startsWith(SHA1_PREFIX)) {
|
||||||
String passwd64 = Base64.encodeBase64String(DigestUtils.sha1(textBytes));
|
String passwd64 = Base64.encodeBase64String(DigestUtils.sha1(textBytes));
|
||||||
return hashStr.substring(SHA1_PREFIX.length()).compareTo(passwd64) == 0;
|
String hashNoPrefix = hashStr.substring(SHA1_PREFIX.length());
|
||||||
|
return SecuredString.constantTimeEquals(passwd64, hashNoPrefix);
|
||||||
}
|
}
|
||||||
if (hashStr.startsWith(SHA2_PREFIX_5) || hashStr.startsWith(SHA2_PREFIX_6)) {
|
if (hashStr.startsWith(SHA2_PREFIX_5) || hashStr.startsWith(SHA2_PREFIX_6)) {
|
||||||
return hashStr.compareTo(Sha2Crypt.sha256Crypt(textBytes, hashStr)) == 0;
|
return SecuredString.constantTimeEquals(hashStr, Sha2Crypt.sha256Crypt(textBytes, hashStr));
|
||||||
}
|
}
|
||||||
return CRYPT_SUPPORTED ?
|
return CRYPT_SUPPORTED ?
|
||||||
hashStr.compareTo(Crypt.crypt(textBytes, hashStr)) == 0 : // crypt algo
|
SecuredString.constantTimeEquals(hashStr, Crypt.crypt(textBytes, hashStr)) : // crypt algo
|
||||||
text.equals(hashStr); // plain text
|
SecuredString.constantTimeEquals(text, hashStr); // plain text
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -197,7 +197,7 @@ public enum Hasher {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
byte[] textBytes = CharArrays.toUtf8Bytes(text.internalChars());
|
byte[] textBytes = CharArrays.toUtf8Bytes(text.internalChars());
|
||||||
return hashStr.compareTo(Md5Crypt.apr1Crypt(textBytes, hashStr)) == 0;
|
return SecuredString.constantTimeEquals(hashStr, Md5Crypt.apr1Crypt(textBytes, hashStr));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -221,7 +221,8 @@ public enum Hasher {
|
||||||
MessageDigest md = SHA1Provider.sha1();
|
MessageDigest md = SHA1Provider.sha1();
|
||||||
md.update(textBytes);
|
md.update(textBytes);
|
||||||
String passwd64 = Base64.encodeBase64String(md.digest());
|
String passwd64 = Base64.encodeBase64String(md.digest());
|
||||||
return hashStr.substring(SHA1_PREFIX.length()).compareTo(passwd64) == 0;
|
String hashNoPrefix = hashStr.substring(SHA1_PREFIX.length());
|
||||||
|
return SecuredString.constantTimeEquals(hashNoPrefix, passwd64);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -237,7 +238,7 @@ public enum Hasher {
|
||||||
String hashStr = new String(hash);
|
String hashStr = new String(hash);
|
||||||
if (hashStr.startsWith(SHA2_PREFIX_5) || hashStr.startsWith(SHA2_PREFIX_6)) {
|
if (hashStr.startsWith(SHA2_PREFIX_5) || hashStr.startsWith(SHA2_PREFIX_6)) {
|
||||||
byte[] textBytes = CharArrays.toUtf8Bytes(text.internalChars());
|
byte[] textBytes = CharArrays.toUtf8Bytes(text.internalChars());
|
||||||
return hashStr.compareTo(Sha2Crypt.sha256Crypt(textBytes, hashStr)) == 0;
|
return SecuredString.constantTimeEquals(hashStr, Sha2Crypt.sha256Crypt(textBytes, hashStr));
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -262,7 +263,7 @@ public enum Hasher {
|
||||||
MessageDigest md = MD5Provider.md5();
|
MessageDigest md = MD5Provider.md5();
|
||||||
md.update(CharArrays.toUtf8Bytes(text.internalChars()));
|
md.update(CharArrays.toUtf8Bytes(text.internalChars()));
|
||||||
String computedHashStr = Base64.encodeBase64String(md.digest());
|
String computedHashStr = Base64.encodeBase64String(md.digest());
|
||||||
return hashStr.equals(computedHashStr);
|
return SecuredString.constantTimeEquals(hashStr, computedHashStr);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -293,7 +294,7 @@ public enum Hasher {
|
||||||
md.update(CharArrays.toUtf8Bytes(text.internalChars()));
|
md.update(CharArrays.toUtf8Bytes(text.internalChars()));
|
||||||
md.update(new String(saltAndHash, 0, 8).getBytes(Charsets.UTF_8));
|
md.update(new String(saltAndHash, 0, 8).getBytes(Charsets.UTF_8));
|
||||||
String computedHash = Base64.encodeBase64String(md.digest());
|
String computedHash = Base64.encodeBase64String(md.digest());
|
||||||
return computedHash.equals(new String(saltAndHash, 8, saltAndHash.length - 8));
|
return SecuredString.constantTimeEquals(computedHash, new String(saltAndHash, 8, saltAndHash.length - 8));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -305,7 +306,7 @@ public enum Hasher {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean verify(SecuredString text, char[] hash) {
|
public boolean verify(SecuredString text, char[] hash) {
|
||||||
return Arrays.equals(text.internalChars(), hash);
|
return SecuredString.constantTimeEquals(text.internalChars(), hash);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -155,4 +155,61 @@ public class SecuredString implements CharSequence {
|
||||||
throw new ElasticsearchException("attempt to use cleared password");
|
throw new ElasticsearchException("attempt to use cleared password");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This does a char by char comparison of the two Strings to provide protection against timing attacks. In other
|
||||||
|
* words it does not exit at the first character that does not match and only exits at the end of the comparison.
|
||||||
|
*
|
||||||
|
* NOTE: length will cause this function to exit early, which is OK as it is not considered feasible to prevent
|
||||||
|
* length attacks
|
||||||
|
*
|
||||||
|
* @param a the first string to be compared
|
||||||
|
* @param b the second string to be compared
|
||||||
|
* @return true if both strings match completely
|
||||||
|
*/
|
||||||
|
public static boolean constantTimeEquals(String a, String b) {
|
||||||
|
char[] aChars = a.toCharArray();
|
||||||
|
char[] bChars = b.toCharArray();
|
||||||
|
|
||||||
|
return constantTimeEquals(aChars, bChars);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This does a char by char comparison of the two Strings to provide protection against timing attacks. In other
|
||||||
|
* words it does not exit at the first character that does not match and only exits at the end of the comparison.
|
||||||
|
*
|
||||||
|
* NOTE: length will cause this function to exit early, which is OK as it is not considered feasible to prevent
|
||||||
|
* length attacks
|
||||||
|
*
|
||||||
|
* @param securedString the securedstring to compare to string char by char
|
||||||
|
* @param string the string to compare
|
||||||
|
* @return true if both match char for char
|
||||||
|
*/
|
||||||
|
public static boolean constantTimeEquals(SecuredString securedString, String string) {
|
||||||
|
return constantTimeEquals(securedString.internalChars(), string.toCharArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This does a char by char comparison of the two arrays to provide protection against timing attacks. In other
|
||||||
|
* words it does not exit at the first character that does not match and only exits at the end of the comparison.
|
||||||
|
*
|
||||||
|
* NOTE: length will cause this function to exit early, which is OK as it is not considered feasible to prevent
|
||||||
|
* length attacks
|
||||||
|
*
|
||||||
|
* @param a the first char array
|
||||||
|
* @param b the second char array
|
||||||
|
* @return true if both match char for char
|
||||||
|
*/
|
||||||
|
public static boolean constantTimeEquals(char[] a, char[] b) {
|
||||||
|
if (a.length != b.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int equals = 0;
|
||||||
|
for (int i = 0; i < a.length; i++) {
|
||||||
|
equals |= a[i] ^ b[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return equals == 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
import static org.elasticsearch.shield.authc.support.SecuredString.constantTimeEquals;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
@ -105,26 +107,34 @@ public class InternalSignatureService extends AbstractLifecycleComponent<Interna
|
||||||
return signedText;
|
return signedText;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!signedText.startsWith("$$")) {
|
if (!signedText.startsWith("$$") || signedText.length() < 2) {
|
||||||
|
throw new SignatureException("tampered signed text");
|
||||||
|
}
|
||||||
|
|
||||||
|
String text;
|
||||||
|
String receivedSignature;
|
||||||
|
try {
|
||||||
|
// $$34$$sigtext
|
||||||
|
int i = signedText.indexOf("$$", 2);
|
||||||
|
int length = Integer.parseInt(signedText.substring(2, i));
|
||||||
|
receivedSignature = signedText.substring(i + 2, i + 2 + length);
|
||||||
|
text = signedText.substring(i + 2 + length);
|
||||||
|
} catch (Throwable t) {
|
||||||
|
logger.error("error occurred while parsing signed text", t);
|
||||||
throw new SignatureException("tampered signed text");
|
throw new SignatureException("tampered signed text");
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// $$34$$sigtext
|
|
||||||
int i = signedText.indexOf("$$", 2);
|
|
||||||
int length = Integer.parseInt(signedText.substring(2, i));
|
|
||||||
String sigStr = signedText.substring(i + 2, i + 2 + length);
|
|
||||||
String text = signedText.substring(i + 2 + length);
|
|
||||||
String sig = signInternal(text);
|
String sig = signInternal(text);
|
||||||
if (!sig.equals(sigStr)) {
|
if (constantTimeEquals(sig, receivedSignature)) {
|
||||||
throw new SignatureException("the signed texts don't match");
|
return text;
|
||||||
}
|
}
|
||||||
return text;
|
|
||||||
} catch (SignatureException e) {
|
|
||||||
throw e;
|
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
throw new SignatureException("error while verifying the signed text", t);
|
logger.error("error occurred while verifying signed text", t);
|
||||||
|
throw new SignatureException("error while verifying the signed text");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
throw new SignatureException("tampered signed text");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -22,6 +22,7 @@ import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
import static org.hamcrest.Matchers.equalTo;
|
import static org.hamcrest.Matchers.equalTo;
|
||||||
import static org.hamcrest.Matchers.is;
|
import static org.hamcrest.Matchers.is;
|
||||||
|
import static org.hamcrest.Matchers.nullValue;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
@ -81,6 +82,72 @@ public class InternalSignatureServiceTests extends ElasticsearchTestCase {
|
||||||
assertThat(text, equalTo(signed));
|
assertThat(text, equalTo(signed));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTamperedSignature() throws Exception {
|
||||||
|
InternalSignatureService service = new InternalSignatureService(settings, env, watcherService).start();
|
||||||
|
String text = randomAsciiOfLength(10);
|
||||||
|
String signed = service.sign(text);
|
||||||
|
int i = signed.indexOf("$$", 2);
|
||||||
|
int length = Integer.parseInt(signed.substring(2, i));
|
||||||
|
String fakeSignature = randomAsciiOfLength(length);
|
||||||
|
String fakeSignedText = "$$" + length + "$$" + fakeSignature + signed.substring(i + 2 + length);
|
||||||
|
|
||||||
|
try {
|
||||||
|
service.unsignAndVerify(fakeSignedText);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
assertThat(e.getMessage(), is(equalTo("tampered signed text")));
|
||||||
|
assertThat(e.getCause(), is(nullValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTamperedSignatureOneChar() throws Exception {
|
||||||
|
InternalSignatureService service = new InternalSignatureService(settings, env, watcherService).start();
|
||||||
|
String text = randomAsciiOfLength(10);
|
||||||
|
String signed = service.sign(text);
|
||||||
|
int i = signed.indexOf("$$", 2);
|
||||||
|
int length = Integer.parseInt(signed.substring(2, i));
|
||||||
|
StringBuilder fakeSignature = new StringBuilder(signed.substring(i + 2, i + 2 + length));
|
||||||
|
fakeSignature.setCharAt(randomIntBetween(0, fakeSignature.length() - 1), randomAsciiOfLength(1).charAt(0));
|
||||||
|
|
||||||
|
String fakeSignedText = "$$" + length + "$$" + fakeSignature.toString() + signed.substring(i + 2 + length);
|
||||||
|
|
||||||
|
try {
|
||||||
|
service.unsignAndVerify(fakeSignedText);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
assertThat(e.getMessage(), is(equalTo("tampered signed text")));
|
||||||
|
assertThat(e.getCause(), is(nullValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testTamperedSignatureLength() throws Exception {
|
||||||
|
InternalSignatureService service = new InternalSignatureService(settings, env, watcherService).start();
|
||||||
|
String text = randomAsciiOfLength(10);
|
||||||
|
String signed = service.sign(text);
|
||||||
|
int i = signed.indexOf("$$", 2);
|
||||||
|
int length = Integer.parseInt(signed.substring(2, i));
|
||||||
|
String fakeSignature = randomAsciiOfLength(length);
|
||||||
|
|
||||||
|
// Smaller sig length
|
||||||
|
String fakeSignedText = "$$" + randomIntBetween(0, length - 1) + "$$" + fakeSignature + signed.substring(i + 2 + length);
|
||||||
|
|
||||||
|
try {
|
||||||
|
service.unsignAndVerify(fakeSignedText);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
assertThat(e.getMessage(), is(equalTo("tampered signed text")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Larger sig length
|
||||||
|
fakeSignedText = "$$" + randomIntBetween(length + 1, Integer.MAX_VALUE) + "$$" + fakeSignature + signed.substring(i + 2 + length);
|
||||||
|
try {
|
||||||
|
service.unsignAndVerify(fakeSignedText);
|
||||||
|
} catch (SignatureException e) {
|
||||||
|
assertThat(e.getMessage(), is(equalTo("tampered signed text")));
|
||||||
|
assertThat(e.getCause(), is(nullValue()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testReloadKey() throws Exception {
|
public void testReloadKey() throws Exception {
|
||||||
final CountDownLatch latch = new CountDownLatch(1);
|
final CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
|
Loading…
Reference in New Issue