HADOOP-14246. Authentication Tokens should use SecureRandom instead of Random and 256 bit secrets

(Conttributed by Robert Konter via Daniel Templeton)
This commit is contained in:
Daniel Templeton 2017-04-12 11:17:31 -07:00
parent b8305e6d06
commit 4dd6206547
4 changed files with 205 additions and 36 deletions

View File

@ -15,8 +15,9 @@ package org.apache.hadoop.security.authentication.util;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import java.nio.charset.Charset; import java.security.SecureRandom;
import java.util.Random; import java.util.Random;
import org.apache.hadoop.classification.InterfaceAudience; import org.apache.hadoop.classification.InterfaceAudience;
import org.apache.hadoop.classification.InterfaceStability; import org.apache.hadoop.classification.InterfaceStability;
@ -32,7 +33,7 @@ public class RandomSignerSecretProvider extends RolloverSignerSecretProvider {
public RandomSignerSecretProvider() { public RandomSignerSecretProvider() {
super(); super();
rand = new Random(); rand = new SecureRandom();
} }
/** /**
@ -48,6 +49,8 @@ public class RandomSignerSecretProvider extends RolloverSignerSecretProvider {
@Override @Override
protected byte[] generateNewSecret() { protected byte[] generateNewSecret() {
return Long.toString(rand.nextLong()).getBytes(Charset.forName("UTF-8")); byte[] secret = new byte[32]; // 32 bytes = 256 bits
rand.nextBytes(secret);
return secret;
} }
} }

View File

@ -16,6 +16,7 @@ package org.apache.hadoop.security.authentication.util;
import com.google.common.annotations.VisibleForTesting; import com.google.common.annotations.VisibleForTesting;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -149,7 +150,7 @@ public class ZKSignerSecretProvider extends RolloverSignerSecretProvider {
public ZKSignerSecretProvider() { public ZKSignerSecretProvider() {
super(); super();
rand = new Random(); rand = new SecureRandom();
} }
/** /**
@ -342,8 +343,11 @@ public class ZKSignerSecretProvider extends RolloverSignerSecretProvider {
} }
} }
private byte[] generateRandomSecret() { @VisibleForTesting
return Long.toString(rand.nextLong()).getBytes(Charset.forName("UTF-8")); protected byte[] generateRandomSecret() {
byte[] secret = new byte[32]; // 32 bytes = 256 bits
rand.nextBytes(secret);
return secret;
} }
/** /**

View File

@ -14,22 +14,37 @@
package org.apache.hadoop.security.authentication.util; package org.apache.hadoop.security.authentication.util;
import java.util.Random; import java.util.Random;
import org.apache.log4j.Level;
import org.apache.log4j.LogManager;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.timeout;
import static org.mockito.Mockito.verify;
public class TestRandomSignerSecretProvider { public class TestRandomSignerSecretProvider {
// rollover every 50 msec
private final int timeout = 100;
private final long rolloverFrequency = timeout / 2;
{
LogManager.getLogger(
RolloverSignerSecretProvider.LOG.getName()).setLevel(Level.DEBUG);
}
@Test @Test
public void testGetAndRollSecrets() throws Exception { public void testGetAndRollSecrets() throws Exception {
long rolloverFrequency = 15 * 1000; // rollover every 15 sec // Use the same seed and a "plain" Random so we can predict the RNG
// use the same seed so we can predict the RNG
long seed = System.currentTimeMillis(); long seed = System.currentTimeMillis();
Random rand = new Random(seed); Random rand = new Random(seed);
byte[] secret1 = Long.toString(rand.nextLong()).getBytes(); byte[] secret1 = generateNewSecret(rand);
byte[] secret2 = Long.toString(rand.nextLong()).getBytes(); byte[] secret2 = generateNewSecret(rand);
byte[] secret3 = Long.toString(rand.nextLong()).getBytes(); byte[] secret3 = generateNewSecret(rand);
RandomSignerSecretProvider secretProvider = MockRandomSignerSecretProvider secretProvider =
new RandomSignerSecretProvider(seed); spy(new MockRandomSignerSecretProvider(seed));
try { try {
secretProvider.init(null, null, rolloverFrequency); secretProvider.init(null, null, rolloverFrequency);
@ -39,7 +54,8 @@ public class TestRandomSignerSecretProvider {
Assert.assertEquals(2, allSecrets.length); Assert.assertEquals(2, allSecrets.length);
Assert.assertArrayEquals(secret1, allSecrets[0]); Assert.assertArrayEquals(secret1, allSecrets[0]);
Assert.assertNull(allSecrets[1]); Assert.assertNull(allSecrets[1]);
Thread.sleep(rolloverFrequency + 2000); verify(secretProvider, timeout(timeout).atLeastOnce()).rollSecret();
secretProvider.realRollSecret();
currentSecret = secretProvider.getCurrentSecret(); currentSecret = secretProvider.getCurrentSecret();
allSecrets = secretProvider.getAllSecrets(); allSecrets = secretProvider.getAllSecrets();
@ -47,7 +63,8 @@ public class TestRandomSignerSecretProvider {
Assert.assertEquals(2, allSecrets.length); Assert.assertEquals(2, allSecrets.length);
Assert.assertArrayEquals(secret2, allSecrets[0]); Assert.assertArrayEquals(secret2, allSecrets[0]);
Assert.assertArrayEquals(secret1, allSecrets[1]); Assert.assertArrayEquals(secret1, allSecrets[1]);
Thread.sleep(rolloverFrequency + 2000); verify(secretProvider, timeout(timeout).atLeast(2)).rollSecret();
secretProvider.realRollSecret();
currentSecret = secretProvider.getCurrentSecret(); currentSecret = secretProvider.getCurrentSecret();
allSecrets = secretProvider.getAllSecrets(); allSecrets = secretProvider.getAllSecrets();
@ -55,9 +72,40 @@ public class TestRandomSignerSecretProvider {
Assert.assertEquals(2, allSecrets.length); Assert.assertEquals(2, allSecrets.length);
Assert.assertArrayEquals(secret3, allSecrets[0]); Assert.assertArrayEquals(secret3, allSecrets[0]);
Assert.assertArrayEquals(secret2, allSecrets[1]); Assert.assertArrayEquals(secret2, allSecrets[1]);
Thread.sleep(rolloverFrequency + 2000); verify(secretProvider, timeout(timeout).atLeast(3)).rollSecret();
secretProvider.realRollSecret();
} finally { } finally {
secretProvider.destroy(); secretProvider.destroy();
} }
} }
/**
* A hack to test RandomSignerSecretProvider.
* We want to test that RandomSignerSecretProvider.rollSecret() is
* periodically called at the expected frequency, but we want to exclude the
* race-condition and not take a long time to run the test.
*/
private class MockRandomSignerSecretProvider
extends RandomSignerSecretProvider {
MockRandomSignerSecretProvider(long seed) {
super(seed);
}
@Override
protected synchronized void rollSecret() {
// this is a no-op: simply used for Mockito to verify that rollSecret()
// is periodically called at the expected frequency
}
public void realRollSecret() {
// the test code manually calls RandomSignerSecretProvider.rollSecret()
// to update the state
super.rollSecret();
}
}
private byte[] generateNewSecret(Random rand) {
byte[] secret = new byte[32];
rand.nextBytes(secret);
return secret;
}
} }

View File

@ -13,13 +13,11 @@
*/ */
package org.apache.hadoop.security.authentication.util; package org.apache.hadoop.security.authentication.util;
import java.util.Arrays; import java.nio.charset.Charset;
import java.util.Properties; import java.util.Properties;
import java.util.Random; import java.util.Random;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.curator.test.TestingServer; import org.apache.curator.test.TestingServer;
import org.apache.log4j.Level; import org.apache.log4j.Level;
import org.apache.log4j.LogManager; import org.apache.log4j.LogManager;
@ -37,13 +35,13 @@ public class TestZKSignerSecretProvider {
private TestingServer zkServer; private TestingServer zkServer;
// rollover every 2 sec // rollover every 50 msec
private final int timeout = 100; private final int timeout = 100;
private final long rolloverFrequency = timeout / 2; private final long rolloverFrequency = timeout / 2;
static final Log LOG = LogFactory.getLog(TestZKSignerSecretProvider.class);
{ {
LogManager.getLogger( RolloverSignerSecretProvider.LOG.getName() ).setLevel(Level.DEBUG); LogManager.getLogger(
RolloverSignerSecretProvider.LOG.getName()).setLevel(Level.DEBUG);
} }
@Before @Before
@ -63,12 +61,12 @@ public class TestZKSignerSecretProvider {
// Test just one ZKSignerSecretProvider to verify that it works in the // Test just one ZKSignerSecretProvider to verify that it works in the
// simplest case // simplest case
public void testOne() throws Exception { public void testOne() throws Exception {
// use the same seed so we can predict the RNG // Use the same seed and a "plain" Random so we can predict the RNG
long seed = System.currentTimeMillis(); long seed = System.currentTimeMillis();
Random rand = new Random(seed); Random rand = new Random(seed);
byte[] secret2 = Long.toString(rand.nextLong()).getBytes(); byte[] secret2 = generateNewSecret(rand);
byte[] secret1 = Long.toString(rand.nextLong()).getBytes(); byte[] secret1 = generateNewSecret(rand);
byte[] secret3 = Long.toString(rand.nextLong()).getBytes(); byte[] secret3 = generateNewSecret(rand);
MockZKSignerSecretProvider secretProvider = MockZKSignerSecretProvider secretProvider =
spy(new MockZKSignerSecretProvider(seed)); spy(new MockZKSignerSecretProvider(seed));
Properties config = new Properties(); Properties config = new Properties();
@ -115,7 +113,7 @@ public class TestZKSignerSecretProvider {
* A hack to test ZKSignerSecretProvider. * A hack to test ZKSignerSecretProvider.
* We want to test that ZKSignerSecretProvider.rollSecret() is periodically * We want to test that ZKSignerSecretProvider.rollSecret() is periodically
* called at the expected frequency, but we want to exclude the * called at the expected frequency, but we want to exclude the
* race-condition. * race-condition and not take a long time to run the test.
*/ */
private class MockZKSignerSecretProvider extends ZKSignerSecretProvider { private class MockZKSignerSecretProvider extends ZKSignerSecretProvider {
MockZKSignerSecretProvider(long seed) { MockZKSignerSecretProvider(long seed) {
@ -134,6 +132,116 @@ public class TestZKSignerSecretProvider {
} }
} }
@Test
// HADOOP-14246 increased the length of the secret from 160 bits to 256 bits.
// This test verifies that the upgrade goes smoothly.
public void testUpgradeChangeSecretLength() throws Exception {
// Use the same seed and a "plain" Random so we can predict the RNG
long seed = System.currentTimeMillis();
Random rand = new Random(seed);
byte[] secret2 = Long.toString(rand.nextLong())
.getBytes(Charset.forName("UTF-8"));
byte[] secret1 = Long.toString(rand.nextLong())
.getBytes(Charset.forName("UTF-8"));
byte[] secret3 = Long.toString(rand.nextLong())
.getBytes(Charset.forName("UTF-8"));
rand = new Random(seed);
// Secrets 4 and 5 get thrown away by ZK when the new secret provider tries
// to init
byte[] secret4 = generateNewSecret(rand);
byte[] secret5 = generateNewSecret(rand);
byte[] secret6 = generateNewSecret(rand);
byte[] secret7 = generateNewSecret(rand);
// Initialize the znode data with the old secret length
MockZKSignerSecretProvider oldSecretProvider =
spy(new OldMockZKSignerSecretProvider(seed));
Properties config = new Properties();
config.setProperty(
ZKSignerSecretProvider.ZOOKEEPER_CONNECTION_STRING,
zkServer.getConnectString());
config.setProperty(ZKSignerSecretProvider.ZOOKEEPER_PATH,
"/secret");
try {
oldSecretProvider.init(config, getDummyServletContext(),
rolloverFrequency);
byte[] currentSecret = oldSecretProvider.getCurrentSecret();
byte[][] allSecrets = oldSecretProvider.getAllSecrets();
Assert.assertArrayEquals(secret1, currentSecret);
Assert.assertEquals(2, allSecrets.length);
Assert.assertArrayEquals(secret1, allSecrets[0]);
Assert.assertNull(allSecrets[1]);
oldSecretProvider.realRollSecret();
currentSecret = oldSecretProvider.getCurrentSecret();
allSecrets = oldSecretProvider.getAllSecrets();
Assert.assertArrayEquals(secret2, currentSecret);
Assert.assertEquals(2, allSecrets.length);
Assert.assertArrayEquals(secret2, allSecrets[0]);
Assert.assertArrayEquals(secret1, allSecrets[1]);
} finally {
oldSecretProvider.destroy();
}
// Now use a ZKSignerSecretProvider with the newer length
MockZKSignerSecretProvider newSecretProvider =
spy(new MockZKSignerSecretProvider(seed));
try {
newSecretProvider.init(config, getDummyServletContext(),
rolloverFrequency);
byte[] currentSecret = newSecretProvider.getCurrentSecret();
byte[][] allSecrets = newSecretProvider.getAllSecrets();
Assert.assertArrayEquals(secret2, currentSecret);
Assert.assertEquals(2, allSecrets.length);
Assert.assertArrayEquals(secret2, allSecrets[0]);
Assert.assertArrayEquals(secret1, allSecrets[1]);
newSecretProvider.realRollSecret();
currentSecret = newSecretProvider.getCurrentSecret();
allSecrets = newSecretProvider.getAllSecrets();
Assert.assertArrayEquals(secret3, currentSecret);
Assert.assertEquals(2, allSecrets.length);
Assert.assertArrayEquals(secret3, allSecrets[0]);
Assert.assertArrayEquals(secret2, allSecrets[1]);
newSecretProvider.realRollSecret();
currentSecret = newSecretProvider.getCurrentSecret();
allSecrets = newSecretProvider.getAllSecrets();
Assert.assertArrayEquals(secret6, currentSecret);
Assert.assertEquals(2, allSecrets.length);
Assert.assertArrayEquals(secret6, allSecrets[0]);
Assert.assertArrayEquals(secret3, allSecrets[1]);
newSecretProvider.realRollSecret();
currentSecret = newSecretProvider.getCurrentSecret();
allSecrets = newSecretProvider.getAllSecrets();
Assert.assertArrayEquals(secret7, currentSecret);
Assert.assertEquals(2, allSecrets.length);
Assert.assertArrayEquals(secret7, allSecrets[0]);
Assert.assertArrayEquals(secret6, allSecrets[1]);
} finally {
newSecretProvider.destroy();
}
}
/**
* A version of {@link MockZKSignerSecretProvider} that uses the old way of
* generating secrets (160 bit long).
*/
private class OldMockZKSignerSecretProvider
extends MockZKSignerSecretProvider {
private Random rand;
OldMockZKSignerSecretProvider(long seed) {
super(seed);
rand = new Random(seed);
}
@Override
protected byte[] generateRandomSecret() {
return Long.toString(rand.nextLong()).getBytes(Charset.forName("UTF-8"));
}
}
@Test @Test
public void testMultiple1() throws Exception { public void testMultiple1() throws Exception {
testMultiple(1); testMultiple(1);
@ -151,19 +259,19 @@ public class TestZKSignerSecretProvider {
* @throws Exception * @throws Exception
*/ */
public void testMultiple(int order) throws Exception { public void testMultiple(int order) throws Exception {
// Use the same seed and a "plain" Random so we can predict the RNG
long seedA = System.currentTimeMillis(); long seedA = System.currentTimeMillis();
Random rand = new Random(seedA); Random rand = new Random(seedA);
byte[] secretA2 = Long.toString(rand.nextLong()).getBytes(); byte[] secretA2 = generateNewSecret(rand);
byte[] secretA1 = Long.toString(rand.nextLong()).getBytes(); byte[] secretA1 = generateNewSecret(rand);
byte[] secretA3 = Long.toString(rand.nextLong()).getBytes(); byte[] secretA3 = generateNewSecret(rand);
byte[] secretA4 = Long.toString(rand.nextLong()).getBytes(); byte[] secretA4 = generateNewSecret(rand);
// use the same seed so we can predict the RNG
long seedB = System.currentTimeMillis() + rand.nextLong(); long seedB = System.currentTimeMillis() + rand.nextLong();
rand = new Random(seedB); rand = new Random(seedB);
byte[] secretB2 = Long.toString(rand.nextLong()).getBytes(); byte[] secretB2 = generateNewSecret(rand);
byte[] secretB1 = Long.toString(rand.nextLong()).getBytes(); byte[] secretB1 = generateNewSecret(rand);
byte[] secretB3 = Long.toString(rand.nextLong()).getBytes(); byte[] secretB3 = generateNewSecret(rand);
byte[] secretB4 = Long.toString(rand.nextLong()).getBytes(); byte[] secretB4 = generateNewSecret(rand);
MockZKSignerSecretProvider secretProviderA = MockZKSignerSecretProvider secretProviderA =
spy(new MockZKSignerSecretProvider(seedA)); spy(new MockZKSignerSecretProvider(seedA));
MockZKSignerSecretProvider secretProviderB = MockZKSignerSecretProvider secretProviderB =
@ -258,4 +366,10 @@ public class TestZKSignerSecretProvider {
.thenReturn(null); .thenReturn(null);
return servletContext; return servletContext;
} }
private byte[] generateNewSecret(Random rand) {
byte[] secret = new byte[32];
rand.nextBytes(secret);
return secret;
}
} }