blob: 788c6e4a5b6bbfb91f898b17ae5397bee23e1469 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package org.apache.qpid.server.security.auth.sasl.scram;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.security.sasl.SaslException;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.apache.qpid.server.model.AuthenticationProvider;
import org.apache.qpid.server.model.Broker;
import org.apache.qpid.server.model.BrokerTestHelper;
import org.apache.qpid.server.model.ConfiguredObject;
import org.apache.qpid.server.model.ConfiguredObjectFactory;
import org.apache.qpid.server.model.PasswordCredentialManagingAuthenticationProvider;
import org.apache.qpid.server.model.State;
import org.apache.qpid.server.security.auth.AuthenticationResult;
import org.apache.qpid.server.security.auth.manager.ScramSHA1AuthenticationManager;
import org.apache.qpid.server.security.auth.manager.ScramSHA256AuthenticationManager;
import org.apache.qpid.server.security.auth.sasl.PasswordSource;
import org.apache.qpid.server.util.Strings;
import org.apache.qpid.test.utils.UnitTestBase;
public class ScramNegotiatorTest extends UnitTestBase
{
private static final String VALID_USER_NAME = "testUser";
private static final String VALID_USER_PASSWORD = "testPassword";
private static final String INVALID_USER_PASSWORD = VALID_USER_PASSWORD + "1";
private static final String INVALID_USER_NAME = VALID_USER_NAME + "1";
private static final String GS2_HEADER = "n,,";
private static final Charset ASCII = Charset.forName("ASCII");
private static final int ITERATION_COUNT = 4096;
private String _clientFirstMessageBare;
private String _clientNonce;
private byte[] _serverSignature;
private PasswordSource _passwordSource;
private AuthenticationProvider<?> _authenticationProvider;
private Broker<?> _broker;
@Before
public void setUp() throws Exception
{
_clientNonce = UUID.randomUUID().toString();
_passwordSource = mock(PasswordSource.class);
when(_passwordSource.getPassword(eq(VALID_USER_NAME))).thenReturn(VALID_USER_PASSWORD.toCharArray());
_authenticationProvider = mock(AuthenticationProvider.class);
_broker = BrokerTestHelper.createBrokerMock();
}
@After
public void tearDown() throws Exception
{
try
{
}
finally
{
_authenticationProvider.close();
}
}
@Test
public void testHandleResponseForScramSha256ValidCredentialsAdapterSource() throws Exception
{
final ScramSaslServerSource scramSaslServerSource =
new ScramSaslServerSourceAdapter(ITERATION_COUNT,
ScramSHA256AuthenticationManager.HMAC_NAME,
ScramSHA256AuthenticationManager.DIGEST_NAME,
_passwordSource);
doSaslNegotiationTestValidCredentials(ScramSHA256AuthenticationManager.MECHANISM,
_authenticationProvider,
scramSaslServerSource);
}
@Test
public void testHandleResponseForScramSha1ValidCredentialsAdapterSource() throws Exception
{
final ScramSaslServerSource scramSaslServerSource =
new ScramSaslServerSourceAdapter(ITERATION_COUNT,
ScramSHA1AuthenticationManager.HMAC_NAME,
ScramSHA1AuthenticationManager.DIGEST_NAME,
_passwordSource);
doSaslNegotiationTestValidCredentials(ScramSHA1AuthenticationManager.MECHANISM,
_authenticationProvider,
scramSaslServerSource);
}
@Test
public void testHandleResponseForScramSha256InvalidPasswordAdapterSource() throws Exception
{
final ScramSaslServerSource scramSaslServerSource =
new ScramSaslServerSourceAdapter(ITERATION_COUNT,
ScramSHA256AuthenticationManager.HMAC_NAME,
ScramSHA256AuthenticationManager.DIGEST_NAME,
_passwordSource);
doSaslNegotiationTestInvalidCredentials(VALID_USER_NAME,
INVALID_USER_PASSWORD,
ScramSHA256AuthenticationManager.MECHANISM,
_authenticationProvider,
scramSaslServerSource);
}
@Test
public void testHandleResponseForScramSha1InvalidPasswordAdapterSource() throws Exception
{
final ScramSaslServerSource scramSaslServerSource =
new ScramSaslServerSourceAdapter(ITERATION_COUNT,
ScramSHA1AuthenticationManager.HMAC_NAME,
ScramSHA1AuthenticationManager.DIGEST_NAME,
_passwordSource);
doSaslNegotiationTestInvalidCredentials(VALID_USER_NAME,
INVALID_USER_PASSWORD,
ScramSHA1AuthenticationManager.MECHANISM,
_authenticationProvider,
scramSaslServerSource);
}
@Test
public void testHandleResponseForScramSha256InvalidUsernameAdapterSource() throws Exception
{
final ScramSaslServerSource scramSaslServerSource =
new ScramSaslServerSourceAdapter(ITERATION_COUNT,
ScramSHA256AuthenticationManager.HMAC_NAME,
ScramSHA256AuthenticationManager.DIGEST_NAME,
_passwordSource);
doSaslNegotiationTestInvalidCredentials(INVALID_USER_NAME,
VALID_USER_PASSWORD,
ScramSHA256AuthenticationManager.MECHANISM,
_authenticationProvider,
scramSaslServerSource);
}
@Test
public void testHandleResponseForScramSha1InvalidUsernameAdapterSource() throws Exception
{
final ScramSaslServerSource scramSaslServerSource =
new ScramSaslServerSourceAdapter(ITERATION_COUNT,
ScramSHA1AuthenticationManager.HMAC_NAME,
ScramSHA1AuthenticationManager.DIGEST_NAME,
_passwordSource);
doSaslNegotiationTestInvalidCredentials(INVALID_USER_NAME,
VALID_USER_PASSWORD,
ScramSHA1AuthenticationManager.MECHANISM,
_authenticationProvider,
scramSaslServerSource);
}
@Test
public void testHandleResponseForScramSha256ValidCredentialsAuthenticationProvider() throws Exception
{
_authenticationProvider = createTestAuthenticationManager(ScramSHA256AuthenticationManager.PROVIDER_TYPE);
doSaslNegotiationTestValidCredentials(ScramSHA256AuthenticationManager.MECHANISM, _authenticationProvider,
(ScramSaslServerSource) _authenticationProvider);
}
@Test
public void testHandleResponseForScramSha1ValidCredentialsAuthenticationProvider() throws Exception
{
_authenticationProvider = createTestAuthenticationManager(ScramSHA1AuthenticationManager.PROVIDER_TYPE);
doSaslNegotiationTestValidCredentials(ScramSHA1AuthenticationManager.MECHANISM, _authenticationProvider,
(ScramSaslServerSource) _authenticationProvider);
}
@Test
public void testHandleResponseForScramSha256InvalidPasswordAuthenticationProvider() throws Exception
{
_authenticationProvider = createTestAuthenticationManager(ScramSHA256AuthenticationManager.PROVIDER_TYPE);
doSaslNegotiationTestInvalidCredentials(VALID_USER_NAME,
INVALID_USER_PASSWORD,
"SCRAM-SHA-256",
_authenticationProvider,
(ScramSaslServerSource) _authenticationProvider);
}
@Test
public void testHandleResponseForScramSha1InvalidPasswordAuthenticationProvider() throws Exception
{
_authenticationProvider = createTestAuthenticationManager(ScramSHA1AuthenticationManager.PROVIDER_TYPE);
doSaslNegotiationTestInvalidCredentials(VALID_USER_NAME,
INVALID_USER_PASSWORD,
"SCRAM-SHA-1",
_authenticationProvider,
(ScramSaslServerSource) _authenticationProvider);
}
@Test
public void testHandleResponseForScramSha256InvalidUsernameAuthenticationProvider() throws Exception
{
_authenticationProvider = createTestAuthenticationManager(ScramSHA256AuthenticationManager.PROVIDER_TYPE);
doSaslNegotiationTestInvalidCredentials(INVALID_USER_NAME,
VALID_USER_PASSWORD,
"SCRAM-SHA-256",
_authenticationProvider,
(ScramSaslServerSource) _authenticationProvider);
}
@Test
public void testHandleResponseForScramSha1InvalidUsernameAuthenticationProvider() throws Exception
{
_authenticationProvider = createTestAuthenticationManager(ScramSHA1AuthenticationManager.PROVIDER_TYPE);
doSaslNegotiationTestInvalidCredentials(INVALID_USER_NAME,
VALID_USER_PASSWORD,
"SCRAM-SHA-1",
_authenticationProvider,
(ScramSaslServerSource) _authenticationProvider);
}
private void doSaslNegotiationTestValidCredentials(final String mechanism,
final AuthenticationProvider<?> authenticationProvider,
final ScramSaslServerSource scramSaslServerSource)
throws Exception
{
ScramNegotiator scramNegotiator = new ScramNegotiator(authenticationProvider,
scramSaslServerSource,
mechanism);
byte[] initialResponse = createInitialResponse(VALID_USER_NAME);
AuthenticationResult firstResult = scramNegotiator.handleResponse(initialResponse);
assertEquals("Unexpected first result status",
AuthenticationResult.AuthenticationStatus.CONTINUE,
firstResult.getStatus());
assertNotNull("Unexpected first result challenge", firstResult.getChallenge());
byte[] response = calculateClientProof(firstResult.getChallenge(),
scramSaslServerSource.getHmacName(),
scramSaslServerSource.getDigestName(),
VALID_USER_PASSWORD);
AuthenticationResult secondResult = scramNegotiator.handleResponse(response);
assertEquals("Unexpected second result status",
AuthenticationResult.AuthenticationStatus.SUCCESS,
secondResult.getStatus());
assertNotNull("Unexpected second result challenge", secondResult.getChallenge());
assertEquals("Unexpected second result principal",
VALID_USER_NAME,
secondResult.getMainPrincipal().getName());
String serverFinalMessage = new String(secondResult.getChallenge(), ASCII);
String[] parts = serverFinalMessage.split(",");
if (!parts[0].startsWith("v="))
{
fail("Server final message did not contain verifier");
}
byte[] serverSignature = Strings.decodeBase64(parts[0].substring(2));
if (!Arrays.equals(_serverSignature, serverSignature))
{
fail("Server signature did not match");
}
AuthenticationResult thirdResult = scramNegotiator.handleResponse(initialResponse);
assertEquals("Unexpected result status after completion of negotiation",
AuthenticationResult.AuthenticationStatus.ERROR,
thirdResult.getStatus());
assertNull("Unexpected principal after completion of negotiation", thirdResult.getMainPrincipal());
}
private void doSaslNegotiationTestInvalidCredentials(final String userName,
final String userPassword,
final String mechanism,
final AuthenticationProvider<?> authenticationProvider,
final ScramSaslServerSource scramSaslServerSource)
throws Exception
{
ScramNegotiator scramNegotiator = new ScramNegotiator(authenticationProvider,
scramSaslServerSource,
mechanism);
byte[] initialResponse = createInitialResponse(userName);
AuthenticationResult firstResult = scramNegotiator.handleResponse(initialResponse);
assertEquals("Unexpected first result status",
AuthenticationResult.AuthenticationStatus.CONTINUE,
firstResult.getStatus());
assertNotNull("Unexpected first result challenge", firstResult.getChallenge());
byte[] response = calculateClientProof(firstResult.getChallenge(),
scramSaslServerSource.getHmacName(),
scramSaslServerSource.getDigestName(),
userPassword);
AuthenticationResult secondResult = scramNegotiator.handleResponse(response);
assertEquals("Unexpected second result status",
AuthenticationResult.AuthenticationStatus.ERROR,
secondResult.getStatus());
assertNull("Unexpected second result challenge", secondResult.getChallenge());
assertNull("Unexpected second result principal", secondResult.getMainPrincipal());
}
private byte[] calculateClientProof(final byte[] challenge,
String hmacName,
String digestName,
String userPassword) throws Exception
{
String serverFirstMessage = new String(challenge, ASCII);
String[] parts = serverFirstMessage.split(",");
if (parts.length < 3)
{
fail("Server challenge '" + serverFirstMessage + "' cannot be parsed");
}
else if (parts[0].startsWith("m="))
{
fail("Server requires mandatory extension which is not supported: " + parts[0]);
}
else if (!parts[0].startsWith("r="))
{
fail("Server challenge '" + serverFirstMessage + "' cannot be parsed, cannot find nonce");
}
String nonce = parts[0].substring(2);
if (!nonce.startsWith(_clientNonce))
{
fail("Server challenge did not use correct client nonce");
}
if (!parts[1].startsWith("s="))
{
fail("Server challenge '" + serverFirstMessage + "' cannot be parsed, cannot find salt");
}
byte[] salt = Strings.decodeBase64(parts[1].substring(2));
if (!parts[2].startsWith("i="))
{
fail("Server challenge '" + serverFirstMessage + "' cannot be parsed, cannot find iteration count");
}
int _iterationCount = Integer.parseInt(parts[2].substring(2));
if (_iterationCount <= 0)
{
fail("Iteration count " + _iterationCount + " is not a positive integer");
}
byte[] passwordBytes = saslPrep(userPassword).getBytes("UTF-8");
byte[] saltedPassword = generateSaltedPassword(passwordBytes, hmacName, _iterationCount, salt);
String clientFinalMessageWithoutProof =
"c=" + Base64.getEncoder().encodeToString(GS2_HEADER.getBytes(ASCII))
+ ",r=" + nonce;
String authMessage = _clientFirstMessageBare + "," + serverFirstMessage + "," + clientFinalMessageWithoutProof;
byte[] clientKey = computeHmac(saltedPassword, "Client Key", hmacName);
byte[] storedKey = MessageDigest.getInstance(digestName).digest(clientKey);
byte[] clientSignature = computeHmac(storedKey, authMessage, hmacName);
byte[] clientProof = clientKey.clone();
for (int i = 0; i < clientProof.length; i++)
{
clientProof[i] ^= clientSignature[i];
}
byte[] serverKey = computeHmac(saltedPassword, "Server Key", hmacName);
_serverSignature = computeHmac(serverKey, authMessage, hmacName);
String finalMessageWithProof = clientFinalMessageWithoutProof
+ ",p=" + Base64.getEncoder().encodeToString(clientProof);
return finalMessageWithProof.getBytes();
}
private byte[] computeHmac(final byte[] key, final String string, String hmacName)
throws Exception
{
Mac mac = createHmac(key, hmacName);
mac.update(string.getBytes(ASCII));
return mac.doFinal();
}
private byte[] generateSaltedPassword(final byte[] passwordBytes,
String hmacName,
final int iterationCount,
final byte[] salt) throws Exception
{
Mac mac = createHmac(passwordBytes, hmacName);
mac.update(salt);
mac.update(new byte[]{0, 0, 0, 1});
byte[] result = mac.doFinal();
byte[] previous = null;
for (int i = 1; i < iterationCount; i++)
{
mac.update(previous != null ? previous : result);
previous = mac.doFinal();
for (int x = 0; x < result.length; x++)
{
result[x] ^= previous[x];
}
}
return result;
}
private Mac createHmac(final byte[] keyBytes, String hmacName) throws Exception
{
SecretKeySpec key = new SecretKeySpec(keyBytes, hmacName);
Mac mac = Mac.getInstance(hmacName);
mac.init(key);
return mac;
}
private String saslPrep(String name) throws SaslException
{
name = name.replace("=", "=3D");
name = name.replace(",", "=2C");
return name;
}
private byte[] createInitialResponse(final String userName) throws SaslException
{
_clientFirstMessageBare = "n=" + saslPrep(userName) + ",r=" + _clientNonce;
return (GS2_HEADER + _clientFirstMessageBare).getBytes(ASCII);
}
private AuthenticationProvider createTestAuthenticationManager(String type)
{
Map<String, Object> attributes = new HashMap<>();
attributes.put(ConfiguredObject.NAME, getTestName());
attributes.put(ConfiguredObject.ID, UUID.randomUUID());
attributes.put(ConfiguredObject.TYPE, type);
ConfiguredObjectFactory objectFactory = _broker.getObjectFactory();
@SuppressWarnings("unchecked")
AuthenticationProvider<?> configuredObject =
objectFactory.create(AuthenticationProvider.class, attributes, _broker);
assertEquals("Unexpected state", State.ACTIVE, configuredObject.getState());
PasswordCredentialManagingAuthenticationProvider<?> authenticationProvider =
(PasswordCredentialManagingAuthenticationProvider<?>) configuredObject;
authenticationProvider.createUser(VALID_USER_NAME,
VALID_USER_PASSWORD,
Collections.<String, String>emptyMap());
return configuredObject;
}
}