ARTEMIS-3106 Support for SASL-SCRAM

adds the implementation necessary to perform SASL-SCRAM authentication with ActiveMQ Artemis
This commit is contained in:
Christoph Läubrich 2021-02-25 06:42:17 +01:00 committed by Clebert Suconic
parent 0edbf890a2
commit 5313a800a3
47 changed files with 5160 additions and 139 deletions

View File

@ -37,6 +37,11 @@
<artifactId>artemis-selector</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-server</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-core-client</artifactId>

View File

@ -63,10 +63,12 @@ import org.apache.activemq.artemis.protocol.amqp.proton.ProtonServerSenderContex
import org.apache.activemq.artemis.protocol.amqp.proton.SenderController;
import org.apache.activemq.artemis.protocol.amqp.sasl.ClientSASL;
import org.apache.activemq.artemis.protocol.amqp.sasl.ClientSASLFactory;
import org.apache.activemq.artemis.protocol.amqp.sasl.scram.SCRAMClientSASL;
import org.apache.activemq.artemis.spi.core.protocol.ConnectionEntry;
import org.apache.activemq.artemis.spi.core.remoting.ClientConnectionLifeCycleListener;
import org.apache.activemq.artemis.spi.core.remoting.ClientProtocolManager;
import org.apache.activemq.artemis.spi.core.remoting.Connection;
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
import org.apache.activemq.artemis.utils.ConfigurationHelper;
import org.apache.activemq.artemis.utils.UUIDGenerator;
import org.apache.qpid.proton.amqp.Symbol;
@ -96,8 +98,8 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
private int retryCounter = 0;
private boolean connecting = false;
private volatile ScheduledFuture reconnectFuture;
private Set<Queue> senders = new HashSet<>();
private Set<Queue> receivers = new HashSet<>();
private final Set<Queue> senders = new HashSet<>();
private final Set<Queue> receivers = new HashSet<>();
final Executor connectExecutor;
final ScheduledExecutorService scheduledExecutorService;
@ -676,7 +678,15 @@ public class AMQPBrokerConnection implements ClientConnectionLifeCycleListener,
if (availableMechanisms.contains(EXTERNAL) && ExternalSASLMechanism.isApplicable(connection)) {
return new ExternalSASLMechanism();
}
if (SCRAMClientSASL.isApplicable(brokerConnectConfiguration.getUser(),
brokerConnectConfiguration.getPassword())) {
for (SCRAM scram : SCRAM.values()) {
if (availableMechanisms.contains(scram.getName())) {
return new SCRAMClientSASL(scram, brokerConnectConfiguration.getUser(),
brokerConnectConfiguration.getPassword());
}
}
}
if (availableMechanisms.contains(PLAIN) && PlainSASLMechanism.isApplicable(brokerConnectConfiguration.getUser(), brokerConnectConfiguration.getPassword())) {
return new PlainSASLMechanism(brokerConnectConfiguration.getUser(), brokerConnectConfiguration.getPassword());
}

View File

@ -0,0 +1,97 @@
/*
* 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.activemq.artemis.protocol.amqp.sasl.scram;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
import java.util.UUID;
import org.apache.activemq.artemis.protocol.amqp.sasl.ClientSASL;
import org.apache.activemq.artemis.protocol.amqp.sasl.scram.ScramClientFunctionality.State;
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
import org.apache.qpid.proton.codec.DecodeException;
/**
* implements the client part of SASL-SCRAM for broker interconnect
*/
public class SCRAMClientSASL implements ClientSASL {
private final SCRAM scramType;
private final ScramClientFunctionalityImpl client;
private final String username;
private final String password;
/**
* @param scram the SCRAM mechanism to use
* @param username the username for authentication
* @param password the password for authentication
*/
public SCRAMClientSASL(SCRAM scram, String username, String password) {
this(scram, username, password, UUID.randomUUID().toString());
}
protected SCRAMClientSASL(SCRAM scram, String username, String password, String nonce) {
Objects.requireNonNull(scram);
Objects.requireNonNull(username);
Objects.requireNonNull(password);
this.username = username;
this.password = password;
this.scramType = scram;
client = new ScramClientFunctionalityImpl(scram.getDigest(), scram.getHmac(), nonce);
}
@Override
public String getName() {
return scramType.getName();
}
@Override
public byte[] getInitialResponse() {
try {
String firstMessage = client.prepareFirstMessage(username);
return firstMessage.getBytes(StandardCharsets.US_ASCII);
} catch (ScramException e) {
throw new DecodeException("prepareFirstMessage failed", e);
}
}
@Override
public byte[] getResponse(byte[] challenge) {
String msg = new String(challenge, StandardCharsets.US_ASCII);
if (client.getState() == State.FIRST_PREPARED) {
try {
String finalMessage = client.prepareFinalMessage(password, msg);
return finalMessage.getBytes(StandardCharsets.US_ASCII);
} catch (ScramException e) {
throw new DecodeException("prepareFinalMessage failed", e);
}
} else if (client.getState() == State.FINAL_PREPARED) {
try {
client.checkServerFinalMessage(msg);
} catch (ScramException e) {
throw new DecodeException("checkServerFinalMessage failed", e);
}
}
return new byte[0];
}
public static boolean isApplicable(String username, String password) {
return username != null && username.length() > 0 && password != null && password.length() > 0;
}
}

View File

@ -0,0 +1,163 @@
/*
* 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.activemq.artemis.protocol.amqp.sasl.scram;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.util.Collections;
import java.util.UUID;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginException;
import org.apache.activemq.artemis.protocol.amqp.sasl.SASLResult;
import org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASL;
import org.apache.activemq.artemis.spi.core.security.jaas.UserPrincipal;
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
import org.apache.activemq.artemis.spi.core.security.scram.UserData;
public abstract class SCRAMServerSASL implements ServerSASL {
protected final ScramServerFunctionality scram;
protected final SCRAM mechanism;
private SASLResult result;
public SCRAMServerSASL(SCRAM mechanism) throws NoSuchAlgorithmException {
this(mechanism, UUID.randomUUID().toString());
}
protected SCRAMServerSASL(SCRAM mechanism, String nonce) throws NoSuchAlgorithmException {
this.mechanism = mechanism;
this.scram = new ScramServerFunctionalityImpl(mechanism.getDigest(), mechanism.getHmac(), nonce);
}
@Override
public String getName() {
return mechanism.getName();
}
@Override
public byte[] processSASL(byte[] bytes) {
String message = new String(bytes, StandardCharsets.US_ASCII);
try {
switch (scram.getState()) {
case INITIAL: {
String userName = scram.handleClientFirstMessage(message);
UserData userData = aquireUserData(userName);
result = new SCRAMSASLResult(userName, scram, createSaslSubject(userName, userData));
String challenge = scram.prepareFirstMessage(userData);
return challenge.getBytes(StandardCharsets.US_ASCII);
}
case PREPARED_FIRST: {
String finalMessage = scram.prepareFinalMessage(message);
return finalMessage.getBytes(StandardCharsets.US_ASCII);
}
default:
result = new SCRAMFailedSASLResult();
break;
}
} catch (GeneralSecurityException | ScramException | RuntimeException e) {
result = new SCRAMFailedSASLResult();
failed(e);
}
return null;
}
protected abstract UserData aquireUserData(String userName) throws LoginException;
protected abstract void failed(Exception e);
protected Subject createSaslSubject(String userName, UserData userData) {
UserPrincipal userPrincipal = new UserPrincipal(userName);
Subject saslSubject = new Subject(true, Collections.singleton(userPrincipal), Collections.singleton(userData),
Collections.emptySet());
return saslSubject;
}
@Override
public SASLResult result() {
if (result instanceof SCRAMSASLResult) {
return scram.isEnded() ? result : null;
}
return result;
}
public boolean isEnded() {
return scram.isEnded();
}
private static final class SCRAMSASLResult implements SASLResult {
private final String userName;
private final ScramServerFunctionality scram;
private final Subject subject;
SCRAMSASLResult(String userName, ScramServerFunctionality scram, Subject subject) {
this.userName = userName;
this.scram = scram;
this.subject = subject;
}
@Override
public String getUser() {
return userName;
}
@Override
public Subject getSubject() {
return subject;
}
@Override
public boolean isSuccess() {
return userName != null && scram.isEnded() && scram.isSuccessful();
}
@Override
public String toString() {
return "SCRAMSASLResult: userName = " + userName + ", state = " + scram.getState();
}
}
private static final class SCRAMFailedSASLResult implements SASLResult {
@Override
public String getUser() {
return null;
}
@Override
public Subject getSubject() {
return null;
}
@Override
public boolean isSuccess() {
return false;
}
@Override
public String toString() {
return "SCRAMFailedSASLResult";
}
}
}

View File

@ -0,0 +1,157 @@
/*
* 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.activemq.artemis.protocol.amqp.sasl.scram;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.Iterator;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;
import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.protocol.amqp.broker.AmqpInterceptor;
import org.apache.activemq.artemis.protocol.amqp.broker.ProtonProtocolManager;
import org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASL;
import org.apache.activemq.artemis.protocol.amqp.sasl.ServerSASLFactory;
import org.apache.activemq.artemis.spi.core.protocol.ProtocolManager;
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
import org.apache.activemq.artemis.spi.core.remoting.Connection;
import org.apache.activemq.artemis.spi.core.security.jaas.DigestCallback;
import org.apache.activemq.artemis.spi.core.security.jaas.HmacCallback;
import org.apache.activemq.artemis.spi.core.security.jaas.SCRAMMechanismCallback;
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
import org.apache.activemq.artemis.spi.core.security.scram.UserData;
import org.jboss.logging.Logger;
/**
* abstract class that implements the SASL-SCRAM authentication scheme, concrete implementations
* must supply the {@link SCRAM} type to use and be register via SPI
*/
public abstract class SCRAMServerSASLFactory implements ServerSASLFactory {
private final Logger logger = Logger.getLogger(getClass());
private final SCRAM scramType;
public SCRAMServerSASLFactory(SCRAM scram) {
this.scramType = scram;
}
@Override
public String getMechanism() {
return scramType.getName();
}
@Override
public boolean isDefaultPermitted() {
return false;
}
@Override
public ServerSASL create(ActiveMQServer server, ProtocolManager<AmqpInterceptor> manager, Connection connection,
RemotingConnection remotingConnection) {
try {
if (manager instanceof ProtonProtocolManager) {
String loginConfigScope = ((ProtonProtocolManager) manager).getSaslLoginConfigScope();
return new JAASSCRAMServerSASL(scramType, loginConfigScope, logger);
}
} catch (NoSuchAlgorithmException e) {
// can't be used then...
}
return null;
}
private static final class JAASSCRAMServerSASL extends SCRAMServerSASL {
private final String loginConfigScope;
private LoginContext loginContext = null;
private Subject loginSubject;
private final Logger logger;
JAASSCRAMServerSASL(SCRAM scram, String loginConfigScope, Logger logger) throws NoSuchAlgorithmException {
super(scram);
this.loginConfigScope = loginConfigScope;
this.logger = logger;
}
@Override
protected UserData aquireUserData(String userName) throws LoginException {
loginContext = new LoginContext(loginConfigScope, new CallbackHandler() {
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
for (Callback callback : callbacks) {
if (callback instanceof NameCallback) {
((NameCallback) callback).setName(userName);
} else if (callback instanceof SCRAMMechanismCallback) {
((SCRAMMechanismCallback) callback).setMechanism(mechanism.getName());
} else if (callback instanceof DigestCallback) {
((DigestCallback) callback).setDigest(scram.getDigest());
} else if (callback instanceof HmacCallback) {
((HmacCallback) callback).setHmac(scram.getHmac());
} else {
throw new UnsupportedCallbackException(callback, "Unrecognized Callback " +
callback.getClass().getSimpleName());
}
}
}
});
loginContext.login();
loginSubject = loginContext.getSubject();
Iterator<UserData> credentials = loginSubject.getPublicCredentials(UserData.class).iterator();
if (credentials.hasNext()) {
return credentials.next();
}
throw new LoginException("can't aquire user data through configured login config scope (" + loginConfigScope +
")");
}
@Override
protected Subject createSaslSubject(String userName, UserData userData) {
if (loginSubject != null) {
return new Subject(true, loginSubject.getPrincipals(), loginSubject.getPublicCredentials(),
loginSubject.getPrivateCredentials());
}
return super.createSaslSubject(userName, userData);
}
@Override
public void done() {
if (loginContext != null) {
try {
loginContext.logout();
} catch (LoginException e1) {
// we can't do anything useful then...
}
}
loginContext = null;
loginSubject = null;
}
@Override
protected void failed(Exception e) {
logger.warn("SASL-SCRAM Authentication failed", e);
}
}
}

View File

@ -0,0 +1,34 @@
/*
* 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.activemq.artemis.protocol.amqp.sasl.scram;
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
/**
* provides SASL SRAM-SHA256
*/
public class SHA256SCRAMServerSASLFactory extends SCRAMServerSASLFactory {
public SHA256SCRAMServerSASLFactory() {
super(SCRAM.SHA256);
}
@Override
public int getPrecedence() {
return 256;
}
}

View File

@ -0,0 +1,34 @@
/*
* 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.activemq.artemis.protocol.amqp.sasl.scram;
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
/**
* provides SASL SRAM-SHA512
*/
public class SHA512SCRAMServerSASLFactory extends SCRAMServerSASLFactory {
public SHA512SCRAMServerSASLFactory() {
super(SCRAM.SHA512);
}
@Override
public int getPrecedence() {
return 512;
}
}

View File

@ -0,0 +1,93 @@
/*
* Copyright 2016 Ognyan Bankov
* <p>
* All rights reserved. Licensed 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.activemq.artemis.protocol.amqp.sasl.scram;
import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
/**
* Provides building blocks for creating SCRAM authentication client
*/
@SuppressWarnings("unused")
public interface ScramClientFunctionality {
/**
* Prepares the first client message
* @param username Username of the user
* @return First client message
* @throws ScramException if username contains prohibited characters
*/
String prepareFirstMessage(String username) throws ScramException;
/**
* Prepares client's final message
* @param password User password
* @param serverFirstMessage Server's first message
* @return Client's final message
* @throws ScramException if there is an error processing server's message, i.e. it violates the
* protocol
*/
String prepareFinalMessage(String password, String serverFirstMessage) throws ScramException;
/**
* Checks if the server's final message is valid
* @param serverFinalMessage Server's final message
* @throws ScramException if there is an error processing server's message, i.e. it violates the
* protocol
*/
void checkServerFinalMessage(String serverFinalMessage) throws ScramException;
/**
* Checks if authentication is successful. You can call this method only if authentication is
* completed. Ensure that using {@link #isEnded()}
* @return true if successful, false otherwise
*/
boolean isSuccessful();
/**
* Checks if authentication is completed, either successfully or not. Authentication is completed
* if {@link #getState()} returns ENDED.
* @return true if authentication has ended
*/
boolean isEnded();
/**
* Gets the state of the authentication procedure
* @return Current state
*/
State getState();
/**
* State of the authentication procedure
*/
enum State {
/**
* Initial state
*/
INITIAL,
/**
* State after first message is prepared
*/
FIRST_PREPARED,
/**
* State after final message is prepared
*/
FINAL_PREPARED,
/**
* Authentication is completes, either successfully or not
*/
ENDED
}
}

View File

@ -0,0 +1,215 @@
/*
* Copyright 2016 Ognyan Bankov
* <p>
* All rights reserved. Licensed 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.activemq.artemis.protocol.amqp.sasl.scram;
import java.nio.charset.Charset;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
import org.apache.activemq.artemis.spi.core.security.scram.ScramUtils;
import org.apache.activemq.artemis.spi.core.security.scram.StringPrep;
/**
* Provides building blocks for creating SCRAM authentication client
*/
@SuppressWarnings("unused")
public class ScramClientFunctionalityImpl implements ScramClientFunctionality {
private static final Pattern SERVER_FIRST_MESSAGE = Pattern.compile("r=([^,]*),s=([^,]*),i=(.*)$");
private static final Pattern SERVER_FINAL_MESSAGE = Pattern.compile("v=([^,]*)$");
private static final String GS2_HEADER = "n,,";
private static final Charset ASCII = Charset.forName("ASCII");
private final String mDigestName;
private final String mHmacName;
private final String mClientNonce;
private String mClientFirstMessageBare;
private final boolean mIsSuccessful = false;
private byte[] mSaltedPassword;
private String mAuthMessage;
private State mState = State.INITIAL;
/**
* Create new ScramClientFunctionalityImpl
* @param digestName Digest to be used
* @param hmacName HMAC to be used
*/
public ScramClientFunctionalityImpl(String digestName, String hmacName) {
this(digestName, hmacName, UUID.randomUUID().toString());
}
/**
* Create new ScramClientFunctionalityImpl
* @param digestName Digest to be used
* @param hmacName HMAC to be used
* @param clientNonce Client nonce to be used
*/
public ScramClientFunctionalityImpl(String digestName, String hmacName, String clientNonce) {
if (ScramUtils.isNullOrEmpty(digestName)) {
throw new NullPointerException("digestName cannot be null or empty");
}
if (ScramUtils.isNullOrEmpty(hmacName)) {
throw new NullPointerException("hmacName cannot be null or empty");
}
if (ScramUtils.isNullOrEmpty(clientNonce)) {
throw new NullPointerException("clientNonce cannot be null or empty");
}
mDigestName = digestName;
mHmacName = hmacName;
mClientNonce = clientNonce;
}
/**
* Prepares first client message You may want to use
* {@link StringPrep#isContainingProhibitedCharacters(String)} in order to check if the username
* contains only valid characters
* @param username Username
* @return prepared first message
* @throws ScramException if <code>username</code> contains prohibited characters
*/
@Override
public String prepareFirstMessage(String username) throws ScramException {
if (mState != State.INITIAL) {
throw new IllegalStateException("You can call this method only once");
}
try {
mClientFirstMessageBare = "n=" + StringPrep.prepAsQueryString(username) + ",r=" + mClientNonce;
mState = State.FIRST_PREPARED;
return GS2_HEADER + mClientFirstMessageBare;
} catch (StringPrep.StringPrepError e) {
mState = State.ENDED;
throw new ScramException("Username contains prohibited character");
}
}
@Override
public String prepareFinalMessage(String password, String serverFirstMessage) throws ScramException {
if (mState != State.FIRST_PREPARED) {
throw new IllegalStateException("You can call this method once only after " + "calling prepareFirstMessage()");
}
Matcher m = SERVER_FIRST_MESSAGE.matcher(serverFirstMessage);
if (!m.matches()) {
mState = State.ENDED;
return null;
}
String nonce = m.group(1);
if (!nonce.startsWith(mClientNonce)) {
mState = State.ENDED;
return null;
}
String salt = m.group(2);
String iterationCountString = m.group(3);
int iterations = Integer.parseInt(iterationCountString);
if (iterations <= 0) {
mState = State.ENDED;
return null;
}
try {
mSaltedPassword = ScramUtils.generateSaltedPassword(password, Base64.getDecoder().decode(salt), iterations,
Mac.getInstance(mHmacName));
String clientFinalMessageWithoutProof =
"c=" + Base64.getEncoder().encodeToString(GS2_HEADER.getBytes(ASCII)) + ",r=" + nonce;
mAuthMessage = mClientFirstMessageBare + "," + serverFirstMessage + "," + clientFinalMessageWithoutProof;
byte[] clientKey = ScramUtils.computeHmac(mSaltedPassword, mHmacName, "Client Key");
byte[] storedKey = MessageDigest.getInstance(mDigestName).digest(clientKey);
byte[] clientSignature = ScramUtils.computeHmac(storedKey, mHmacName, mAuthMessage);
byte[] clientProof = clientKey.clone();
for (int i = 0; i < clientProof.length; i++) {
clientProof[i] ^= clientSignature[i];
}
mState = State.FINAL_PREPARED;
return clientFinalMessageWithoutProof + ",p=" + Base64.getEncoder().encodeToString(clientProof);
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
mState = State.ENDED;
throw new ScramException(e);
}
}
@Override
public void checkServerFinalMessage(String serverFinalMessage) throws ScramException {
if (mState != State.FINAL_PREPARED) {
throw new IllegalStateException("You can call this method only once after " + "calling prepareFinalMessage()");
}
Matcher m = SERVER_FINAL_MESSAGE.matcher(serverFinalMessage);
if (!m.matches()) {
mState = State.ENDED;
throw new ScramException("invalid message format");
}
byte[] serverSignature = Base64.getDecoder().decode(m.group(1));
mState = State.ENDED;
if (!Arrays.equals(serverSignature, getExpectedServerSignature())) {
throw new ScramException("Server signature missmatch");
}
}
@Override
public boolean isSuccessful() {
if (mState == State.ENDED) {
return mIsSuccessful;
} else {
throw new IllegalStateException("You cannot call this method before authentication is ended. " +
"Use isEnded() to check that");
}
}
@Override
public boolean isEnded() {
return mState == State.ENDED;
}
@Override
public State getState() {
return mState;
}
private byte[] getExpectedServerSignature() throws ScramException {
try {
byte[] serverKey = ScramUtils.computeHmac(mSaltedPassword, mHmacName, "Server Key");
return ScramUtils.computeHmac(serverKey, mHmacName, mAuthMessage);
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
mState = State.ENDED;
throw new ScramException(e);
}
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright 2016 Ognyan Bankov
* <p>
* All rights reserved. Licensed 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.activemq.artemis.protocol.amqp.sasl.scram;
import java.security.MessageDigest;
import javax.crypto.Mac;
import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
import org.apache.activemq.artemis.spi.core.security.scram.UserData;
/**
* Provides building blocks for creating SCRAM authentication server
*/
public interface ScramServerFunctionality {
/**
* Handles client's first message
* @param message Client's first message
* @return username extracted from the client message
* @throws ScramException
*/
String handleClientFirstMessage(String message) throws ScramException;
/**
* Prepares server's first message
* @param userData user data needed to prepare the message
* @return Server's first message
*/
String prepareFirstMessage(UserData userData);
/**
* Prepares server's final message
* @param clientFinalMessage Client's final message
* @return Server's final message
* @throws ScramException
*/
String prepareFinalMessage(String clientFinalMessage) throws ScramException;
/**
* Checks if authentication is completed, either successfully or not. Authentication is completed
* if {@link #getState()} returns ENDED.
* @return true if authentication has ended
*/
boolean isSuccessful();
/**
* Checks if authentication is completed, either successfully or not. Authentication is completed
* if {@link #getState()} returns ENDED.
* @return true if authentication has ended
*/
boolean isEnded();
/**
* Gets the state of the authentication procedure
* @return Current state
*/
State getState();
/**
* State of the authentication procedure
*/
enum State {
/**
* Initial state
*/
INITIAL,
/**
* First client message is handled (username is extracted)
*/
FIRST_CLIENT_MESSAGE_HANDLED,
/**
* First server message is prepared
*/
PREPARED_FIRST,
/**
* Authentication is completes, either successfully or not
*/
ENDED
}
MessageDigest getDigest();
Mac getHmac();
}

View File

@ -0,0 +1,204 @@
/*
* Copyright 2016 Ognyan Bankov
* <p>
* All rights reserved. Licensed 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.activemq.artemis.protocol.amqp.sasl.scram;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
import java.util.UUID;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.crypto.Mac;
import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
import org.apache.activemq.artemis.spi.core.security.scram.ScramUtils;
import org.apache.activemq.artemis.spi.core.security.scram.UserData;
/**
* Provides building blocks for creating SCRAM authentication server
*/
public class ScramServerFunctionalityImpl implements ScramServerFunctionality {
private static final Pattern CLIENT_FIRST_MESSAGE =
Pattern.compile("^(([pny])=?([^,]*),([^,]*),)(m?=?[^,]*,?n=([^,]*),r=([^,]*),?.*)$");
private static final Pattern CLIENT_FINAL_MESSAGE = Pattern.compile("(c=([^,]*),r=([^,]*)),p=(.*)$");
private final String mServerPartNonce;
private boolean mIsSuccessful = false;
private State mState = State.INITIAL;
private String mClientFirstMessageBare;
private String mNonce;
private String mServerFirstMessage;
private UserData mUserData;
private final MessageDigest digest;
private final Mac hmac;
/**
* Creates new ScramServerFunctionalityImpl
* @param digestName Digest to be used
* @param hmacName HMAC to be used
* @throws NoSuchAlgorithmException
*/
public ScramServerFunctionalityImpl(String digestName, String hmacName) throws NoSuchAlgorithmException {
this(digestName, hmacName, UUID.randomUUID().toString());
}
/**
* /** Creates new ScramServerFunctionalityImpl
* @param digestName Digest to be used
* @param hmacName HMAC to be used
* @param serverPartNonce Server's part of the nonce
* @throws NoSuchAlgorithmException
*/
public ScramServerFunctionalityImpl(String digestName, String hmacName,
String serverPartNonce) throws NoSuchAlgorithmException {
if (ScramUtils.isNullOrEmpty(digestName)) {
throw new NullPointerException("digestName cannot be null or empty");
}
if (ScramUtils.isNullOrEmpty(hmacName)) {
throw new NullPointerException("hmacName cannot be null or empty");
}
if (ScramUtils.isNullOrEmpty(serverPartNonce)) {
throw new NullPointerException("serverPartNonce cannot be null or empty");
}
digest = MessageDigest.getInstance(digestName);
hmac = Mac.getInstance(hmacName);
mServerPartNonce = serverPartNonce;
}
/**
* Handles client's first message
* @param message Client's first message
* @return username extracted from the client message
* @throws ScramException
*/
@Override
public String handleClientFirstMessage(String message) throws ScramException {
Matcher m = CLIENT_FIRST_MESSAGE.matcher(message);
if (!m.matches()) {
mState = State.ENDED;
throw new ScramException("Invalid message received");
}
mClientFirstMessageBare = m.group(5);
String username = m.group(6);
String clientNonce = m.group(7);
mNonce = clientNonce + mServerPartNonce;
mState = State.FIRST_CLIENT_MESSAGE_HANDLED;
return username;
}
@Override
public String prepareFirstMessage(UserData userData) {
mUserData = userData;
mState = State.PREPARED_FIRST;
mServerFirstMessage = String.format("r=%s,s=%s,i=%d", mNonce, userData.salt, userData.iterations);
return mServerFirstMessage;
}
@Override
public String prepareFinalMessage(String clientFinalMessage) throws ScramException {
String finalMessage = prepareFinalMessageUnchecked(clientFinalMessage);
if (!mIsSuccessful) {
throw new ScramException("client credentials missmatch");
}
return finalMessage;
}
public String prepareFinalMessageUnchecked(String clientFinalMessage) throws ScramException {
mState = State.ENDED;
Matcher m = CLIENT_FINAL_MESSAGE.matcher(clientFinalMessage);
if (!m.matches()) {
throw new ScramException("Invalid message received");
}
String clientFinalMessageWithoutProof = m.group(1);
String clientNonce = m.group(3);
String proof = m.group(4);
if (!mNonce.equals(clientNonce)) {
throw new ScramException("Nonce mismatch");
}
String authMessage = mClientFirstMessageBare + "," + mServerFirstMessage + "," + clientFinalMessageWithoutProof;
byte[] storedKeyArr = Base64.getDecoder().decode(mUserData.storedKey);
byte[] clientSignature = ScramUtils.computeHmac(storedKeyArr, hmac, authMessage);
byte[] serverSignature =
ScramUtils.computeHmac(Base64.getDecoder().decode(mUserData.serverKey), hmac, authMessage);
byte[] clientKey = clientSignature.clone();
byte[] decodedProof = Base64.getDecoder().decode(proof);
for (int i = 0; i < clientKey.length; i++) {
clientKey[i] ^= decodedProof[i];
}
byte[] resultKey = digest.digest(clientKey);
mIsSuccessful = Arrays.equals(storedKeyArr, resultKey);
return "v=" + Base64.getEncoder().encodeToString(serverSignature);
}
@Override
public boolean isSuccessful() {
if (mState == State.ENDED) {
return mIsSuccessful;
} else {
throw new IllegalStateException("You cannot call this method before authentication is ended. " +
"Use isEnded() to check that");
}
}
@Override
public boolean isEnded() {
return mState == State.ENDED;
}
@Override
public State getState() {
return mState;
}
@Override
public MessageDigest getDigest() {
try {
return (MessageDigest) digest.clone();
} catch (CloneNotSupportedException cns) {
try {
return MessageDigest.getInstance(digest.getAlgorithm());
} catch (NoSuchAlgorithmException nsa) {
throw new AssertionError(nsa);
}
}
}
@Override
public Mac getHmac() {
try {
return (Mac) hmac.clone();
} catch (CloneNotSupportedException cns) {
try {
return Mac.getInstance(hmac.getAlgorithm());
} catch (NoSuchAlgorithmException nsa) {
throw new AssertionError(nsa);
}
}
}
}

View File

@ -1,4 +1,6 @@
org.apache.activemq.artemis.protocol.amqp.sasl.AnonymousServerSASLFactory
org.apache.activemq.artemis.protocol.amqp.sasl.PlainServerSASLFactory
org.apache.activemq.artemis.protocol.amqp.sasl.GSSAPIServerSASLFactory
org.apache.activemq.artemis.protocol.amqp.sasl.ExternalServerSASLFactory
org.apache.activemq.artemis.protocol.amqp.sasl.ExternalServerSASLFactory
org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA256SCRAMServerSASLFactory
org.apache.activemq.artemis.protocol.amqp.sasl.scram.SHA512SCRAMServerSASLFactory

View File

@ -0,0 +1,192 @@
/*
* 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.activemq.artemis.protocol.amqp.sasl;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import javax.crypto.Mac;
import javax.security.auth.login.LoginException;
import org.apache.activemq.artemis.protocol.amqp.sasl.scram.SCRAMClientSASL;
import org.apache.activemq.artemis.protocol.amqp.sasl.scram.SCRAMServerSASL;
import org.apache.activemq.artemis.protocol.amqp.sasl.scram.ScramServerFunctionalityImpl;
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
import org.apache.activemq.artemis.spi.core.security.scram.ScramUtils;
import org.apache.activemq.artemis.spi.core.security.scram.UserData;
import org.apache.qpid.proton.codec.DecodeException;
import org.hamcrest.core.IsInstanceOf;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;
/**
* test cases for the SASL-SCRAM
*/
@RunWith(Parameterized.class)
public class SCRAMTest {
/**
*
*/
private final SCRAM mechanism;
private static final byte[] SALT = new byte[32];
private static final String SNONCE = "server";
private static final String CNONCE = "client";
private static final String USERNAME = "test";
private static final String PASSWORD = "123";
@Parameters(name = "{0}")
public static List<Object[]> data() {
List<Object[]> list = new ArrayList<>();
for (SCRAM scram : SCRAM.values()) {
list.add(new Object[] {scram});
}
return list;
}
public SCRAMTest(SCRAM mechanism) {
this.mechanism = mechanism;
}
@Test
public void testSuccess() throws NoSuchAlgorithmException {
TestSCRAMServerSASL serverSASL = new TestSCRAMServerSASL(mechanism, USERNAME, PASSWORD);
TestSCRAMClientSASL clientSASL = new TestSCRAMClientSASL(mechanism, USERNAME, PASSWORD);
byte[] clientFirst = clientSASL.getInitialResponse();
assertNotNull(clientFirst);
byte[] serverFirst = serverSASL.processSASL(clientFirst);
assertNotNull(serverFirst);
assertNull(serverSASL.result());
byte[] clientFinal = clientSASL.getResponse(serverFirst);
assertNotNull(clientFinal);
assertFalse(clientFinal.length == 0);
byte[] serverFinal = serverSASL.processSASL(clientFinal);
assertNotNull(serverFinal);
assertNotNull(serverSASL.result());
assertNotNull(serverSASL.result().getSubject());
assertEquals(USERNAME, serverSASL.result().getUser());
assertNull(serverSASL.exception);
assertTrue(serverSASL.result().isSuccess());
byte[] clientCheck = clientSASL.getResponse(serverFinal);
assertNotNull(clientCheck);
assertTrue(clientCheck.length == 0);
}
@Test
public void testWrongClientPassword() throws NoSuchAlgorithmException {
TestSCRAMServerSASL serverSASL = new TestSCRAMServerSASL(mechanism, USERNAME, PASSWORD);
TestSCRAMClientSASL clientSASL = new TestSCRAMClientSASL(mechanism, USERNAME, "xyz");
byte[] clientFirst = clientSASL.getInitialResponse();
assertNotNull(clientFirst);
byte[] serverFirst = serverSASL.processSASL(clientFirst);
assertNotNull(serverFirst);
assertNull(serverSASL.result());
byte[] clientFinal = clientSASL.getResponse(serverFirst);
assertNotNull(clientFinal);
assertFalse(clientFinal.length == 0);
byte[] serverFinal = serverSASL.processSASL(clientFinal);
assertNull(serverFinal);
assertNotNull(serverSASL.result());
assertFalse(serverSASL.result().isSuccess());
assertThat(serverSASL.exception, IsInstanceOf.instanceOf(ScramException.class));
}
@Test(expected = DecodeException.class)
public void testServerTryTrickClient() throws NoSuchAlgorithmException, ScramException {
TestSCRAMClientSASL clientSASL = new TestSCRAMClientSASL(mechanism, USERNAME, PASSWORD);
ScramServerFunctionalityImpl bad =
new ScramServerFunctionalityImpl(mechanism.getDigest(), mechanism.getHmac(), SNONCE);
byte[] clientFirst = clientSASL.getInitialResponse();
assertNotNull(clientFirst);
bad.handleClientFirstMessage(new String(clientFirst, StandardCharsets.US_ASCII));
byte[] serverFirst =
bad.prepareFirstMessage(generateUserData(mechanism, "bad")).getBytes(StandardCharsets.US_ASCII);
byte[] clientFinal = clientSASL.getResponse(serverFirst);
assertNotNull(clientFinal);
assertFalse(clientFinal.length == 0);
byte[] serverFinal = bad.prepareFinalMessageUnchecked(new String(clientFinal, StandardCharsets.US_ASCII))
.getBytes(StandardCharsets.US_ASCII);
clientSASL.getResponse(serverFinal);
}
private static UserData generateUserData(SCRAM mechanism, String password) throws NoSuchAlgorithmException,
ScramException {
MessageDigest digest = MessageDigest.getInstance(mechanism.getDigest());
Mac hmac = Mac.getInstance(mechanism.getHmac());
ScramUtils.NewPasswordStringData data =
ScramUtils.byteArrayToStringData(ScramUtils.newPassword(password, SALT, 4096, digest, hmac));
return new UserData(data.salt, data.iterations, data.serverKey, data.storedKey);
}
private static final class TestSCRAMClientSASL extends SCRAMClientSASL {
TestSCRAMClientSASL(SCRAM scram, String username, String password) {
super(scram, username, password, CNONCE);
}
}
private static final class TestSCRAMServerSASL extends SCRAMServerSASL {
private Exception exception;
private final String username;
private final String password;
TestSCRAMServerSASL(SCRAM mechanism, String username, String password) throws NoSuchAlgorithmException {
super(mechanism, SNONCE);
this.username = username;
this.password = password;
}
@Override
public void done() {
// nothing to do
}
@Override
protected UserData aquireUserData(String userName) throws LoginException {
if (!this.username.equals(userName)) {
throw new LoginException("invalid username");
}
try {
return generateUserData(mechanism, password);
} catch (Exception e) {
throw new LoginException(e.getMessage());
}
}
@Override
protected void failed(Exception e) {
this.exception = e;
}
}
}

View File

@ -0,0 +1,120 @@
/*
* 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.activemq.artemis.spi.core.security.jaas;
import java.io.IOException;
import java.security.Principal;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import org.jboss.logging.Logger;
/**
* Abstract login module that uses an external authenticated principal
*/
public abstract class AbstractPrincipalLoginModule implements AuditLoginModule {
private final Logger logger = Logger.getLogger(getClass());
private Subject subject;
private final List<Principal> authenticatedPrincipals = new LinkedList<>();
private CallbackHandler callbackHandler;
private boolean loginSucceeded;
private Principal[] principals;
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
Map<String, ?> options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
}
@Override
public boolean login() throws LoginException {
Callback[] callbacks = new Callback[1];
callbacks[0] = new PrincipalsCallback();
try {
callbackHandler.handle(callbacks);
principals = ((PrincipalsCallback) callbacks[0]).getPeerPrincipals();
if (principals != null) {
authenticatedPrincipals.addAll(Arrays.asList(principals));
}
} catch (IOException ioe) {
throw new LoginException(ioe.getMessage());
} catch (UnsupportedCallbackException uce) {
throw new LoginException(uce.getMessage() + " not available to obtain information from user");
}
if (!authenticatedPrincipals.isEmpty()) {
loginSucceeded = true;
}
logger.debug("login " + authenticatedPrincipals);
return loginSucceeded;
}
@Override
public boolean commit() throws LoginException {
boolean result = loginSucceeded;
if (result) {
authenticatedPrincipals.add(new UserPrincipal(authenticatedPrincipals.get(0).getName()));
subject.getPrincipals().addAll(authenticatedPrincipals);
}
clear();
logger.debug("commit, result: " + result);
return result;
}
@Override
public boolean abort() throws LoginException {
if (principals != null) {
for (Principal principal : authenticatedPrincipals) {
registerFailureForAudit(principal.getName());
}
}
clear();
logger.debug("abort");
return true;
}
private void clear() {
principals = null;
loginSucceeded = false;
}
@Override
public boolean logout() throws LoginException {
subject.getPrincipals().removeAll(authenticatedPrincipals);
authenticatedPrincipals.clear();
clear();
logger.debug("logout");
return true;
}
}

View File

@ -0,0 +1,45 @@
/*
* 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.activemq.artemis.spi.core.security.jaas;
import java.security.MessageDigest;
import javax.security.auth.callback.Callback;
/**
* Callback to obtain a {@link MessageDigest} for login purpose
*/
public class DigestCallback implements Callback {
private MessageDigest digest;
/**
* set the digest to use
* @param digest the digest
*/
public void setDigest(MessageDigest digest) {
this.digest = digest;
}
/**
* @return the digest or <code>null</code> if not known
*/
public MessageDigest getDigest() {
return digest;
}
}

View File

@ -0,0 +1,44 @@
/*
* 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.activemq.artemis.spi.core.security.jaas;
import javax.crypto.Mac;
import javax.security.auth.callback.Callback;
/**
* Callback for obtaining information about a used H{@link Mac}
*/
public class HmacCallback implements Callback {
private Mac hmac;
/**
* set the Hmac to use
* @param hmac
*/
public void setHmac(Mac hmac) {
this.hmac = hmac;
}
/**
* @return the Hmac or <code>null</code> if non could be obtained
*/
public Mac getHmac() {
return hmac;
}
}

View File

@ -16,7 +16,12 @@
*/
package org.apache.activemq.artemis.spi.core.security.jaas;
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
import static org.apache.activemq.artemis.core.remoting.CertificateUtil.getCertsFromConnection;
import static org.apache.activemq.artemis.core.remoting.CertificateUtil.getPeerPrincipalFromConnection;
import java.io.IOException;
import java.security.Principal;
import java.util.Set;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
@ -25,11 +30,8 @@ import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.PasswordCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.kerberos.KerberosPrincipal;
import java.io.IOException;
import java.security.Principal;
import static org.apache.activemq.artemis.core.remoting.CertificateUtil.getCertsFromConnection;
import static org.apache.activemq.artemis.core.remoting.CertificateUtil.getPeerPrincipalFromConnection;
import org.apache.activemq.artemis.spi.core.protocol.RemotingConnection;
/**
* A JAAS username password CallbackHandler.
@ -67,18 +69,26 @@ public class JaasCallbackHandler implements CallbackHandler {
CertificateCallback certCallback = (CertificateCallback) callback;
certCallback.setCertificates(getCertsFromConnection(remotingConnection));
} else if (callback instanceof Krb5Callback) {
Krb5Callback krb5Callback = (Krb5Callback) callback;
} else if (callback instanceof PrincipalsCallback) {
PrincipalsCallback principalsCallback = (PrincipalsCallback) callback;
Subject peerSubject = remotingConnection.getSubject();
if (peerSubject != null) {
for (Principal principal : peerSubject.getPrivateCredentials(KerberosPrincipal.class)) {
krb5Callback.setPeerPrincipal(principal);
for (KerberosPrincipal principal : peerSubject.getPrivateCredentials(KerberosPrincipal.class)) {
principalsCallback.setPeerPrincipals(new Principal[] {principal});
return;
}
Set<Principal> principals = peerSubject.getPrincipals();
if (principals.size() > 0) {
principalsCallback.setPeerPrincipals(principals.toArray(new Principal[0]));
return;
}
}
krb5Callback.setPeerPrincipal(getPeerPrincipalFromConnection(remotingConnection));
Principal peerPrincipalFromConnection = getPeerPrincipalFromConnection(remotingConnection);
if (peerPrincipalFromConnection != null) {
principalsCallback.setPeerPrincipals(new Principal[] {peerPrincipalFromConnection});
}
} else {
throw new UnsupportedCallbackException(callback);
}

View File

@ -16,102 +16,10 @@
*/
package org.apache.activemq.artemis.spi.core.security.jaas;
import org.jboss.logging.Logger;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import java.io.IOException;
import java.security.Principal;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
/**
* populate a subject with kerberos credential from the handler
*/
public class Krb5LoginModule implements AuditLoginModule {
public class Krb5LoginModule extends AbstractPrincipalLoginModule {
private static final Logger logger = Logger.getLogger(Krb5LoginModule.class);
private Subject subject;
private final List<Principal> principals = new LinkedList<>();
private CallbackHandler callbackHandler;
private boolean loginSucceeded;
private Principal principal;
@Override
public void initialize(Subject subject,
CallbackHandler callbackHandler,
Map<String, ?> sharedState,
Map<String, ?> options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
}
@Override
public boolean login() throws LoginException {
Callback[] callbacks = new Callback[1];
callbacks[0] = new Krb5Callback();
try {
callbackHandler.handle(callbacks);
principal = ((Krb5Callback)callbacks[0]).getPeerPrincipal();
if (principal != null) {
principals.add(principal);
}
} catch (IOException ioe) {
throw new LoginException(ioe.getMessage());
} catch (UnsupportedCallbackException uce) {
throw new LoginException(uce.getMessage() + " not available to obtain information from user");
}
if (!principals.isEmpty()) {
loginSucceeded = true;
}
logger.debug("login " + principals);
return loginSucceeded;
}
@Override
public boolean commit() throws LoginException {
boolean result = loginSucceeded;
if (result) {
principals.add(new UserPrincipal(principals.get(0).getName()));
subject.getPrincipals().addAll(principals);
}
clear();
logger.debug("commit, result: " + result);
return result;
}
@Override
public boolean abort() throws LoginException {
registerFailureForAudit(principal != null ? principal.getName() : null);
clear();
logger.debug("abort");
return true;
}
private void clear() {
principal = null;
loginSucceeded = false;
}
@Override
public boolean logout() throws LoginException {
subject.getPrincipals().removeAll(principals);
principals.clear();
clear();
logger.debug("logout");
return true;
}
}

View File

@ -16,31 +16,30 @@
*/
package org.apache.activemq.artemis.spi.core.security.jaas;
import javax.security.auth.callback.Callback;
import java.security.Principal;
/**
* A Callback for kerberos peer principal.
*/
public class Krb5Callback implements Callback {
import javax.security.auth.callback.Callback;
Principal peerPrincipal;
/**
* A Callback for getting the peer principals.
*/
public class PrincipalsCallback implements Callback {
Principal[] peerPrincipals;
/**
* Setter for peer Principal.
*
* Setter for peer Principals.
* @param principal The certificates to be returned.
*/
public void setPeerPrincipal(Principal principal) {
peerPrincipal = principal;
public void setPeerPrincipals(Principal[] principal) {
peerPrincipals = principal;
}
/**
* Getter for peer Principal.
*
* Getter for peer Principals.
* @return The principal being carried.
*/
public Principal getPeerPrincipal() {
return peerPrincipal;
public Principal[] getPeerPrincipals() {
return peerPrincipals;
}
}

View File

@ -0,0 +1,25 @@
/*
* 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.activemq.artemis.spi.core.security.jaas;
/**
* Handles the actual login after channel authentication has succeed
*/
public class SCRAMLoginModule extends AbstractPrincipalLoginModule {
}

View File

@ -0,0 +1,44 @@
/*
* 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.activemq.artemis.spi.core.security.jaas;
import javax.security.auth.callback.Callback;
/**
* callback to obtain the a mechanism used in a SASL-SCRAM authentication
*/
public class SCRAMMechanismCallback implements Callback {
private String name;
/**
* sets the name of the mechanism
* @param name the name of the mechanism
*/
public void setMechanism(String name) {
this.name = name;
}
/**
* @return the name of the mechanism
*/
public String getMechanism() {
return name;
}
}

View File

@ -0,0 +1,237 @@
/*
* 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.activemq.artemis.spi.core.security.jaas;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.Principal;
import java.security.SecureRandom;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import javax.crypto.Mac;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.NameCallback;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.auth.login.LoginException;
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
import org.apache.activemq.artemis.spi.core.security.scram.ScramException;
import org.apache.activemq.artemis.spi.core.security.scram.ScramUtils;
import org.apache.activemq.artemis.spi.core.security.scram.StringPrep;
import org.apache.activemq.artemis.spi.core.security.scram.StringPrep.StringPrepError;
import org.apache.activemq.artemis.spi.core.security.scram.UserData;
import org.apache.activemq.artemis.utils.PasswordMaskingUtil;
/**
* Login modules that uses properties files similar to the {@link PropertiesLoginModule}. It can
* either store the username-password in plain text or in an encrypted/hashed form. the
* {@link #main(String[])} method provides a way to prepare unencrypted data to be encrypted/hashed.
*/
public class SCRAMPropertiesLoginModule extends PropertiesLoader implements AuditLoginModule {
/**
*
*/
private static final String SEPARATOR_MECHANISM = "|";
private static final String SEPARATOR_PARAMETER = ":";
private static final int MIN_ITERATIONS = 4096;
private static final SecureRandom RANDOM_GENERATOR = new SecureRandom();
private Subject subject;
private CallbackHandler callbackHandler;
private Properties users;
private Map<String, Set<String>> roles;
private UserData userData;
private String user;
private final Set<Principal> principals = new HashSet<>();
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, Map<String, ?> sharedState,
Map<String, ?> options) {
this.subject = subject;
this.callbackHandler = callbackHandler;
init(options);
users = load(PropertiesLoginModule.USER_FILE_PROP_NAME, "user", options).getProps();
roles = load(PropertiesLoginModule.ROLE_FILE_PROP_NAME, "role", options).invertedPropertiesValuesMap();
}
@Override
public boolean login() throws LoginException {
NameCallback nameCallback = new NameCallback("Username: ");
executeCallbacks(nameCallback);
user = nameCallback.getName();
SCRAMMechanismCallback mechanismCallback = new SCRAMMechanismCallback();
executeCallbacks(mechanismCallback);
SCRAM scram = getTypeByString(mechanismCallback.getMechanism());
if (user == null) {
userData = generateUserData(null); // generate random user data
} else {
String password = users.getProperty(user + SEPARATOR_MECHANISM + scram.name());
if (password == null) {
// fallback for probably unencoded user/password or a single encoded entry
password = users.getProperty(user);
}
if (PasswordMaskingUtil.isEncMasked(password)) {
String[] unwrap = PasswordMaskingUtil.unwrap(password).split(SEPARATOR_PARAMETER);
userData = new UserData(unwrap[0], Integer.parseInt(unwrap[1]), unwrap[2], unwrap[3]);
} else {
userData = generateUserData(password);
}
}
return true;
}
private UserData generateUserData(String plainTextPassword) throws LoginException {
if (plainTextPassword == null) {
// if the user is not available (or the password) generate a random password here so an
// attacker can't
// distinguish between a missing username and a wrong password
byte[] randomPassword = new byte[256];
RANDOM_GENERATOR.nextBytes(randomPassword);
plainTextPassword = new String(randomPassword);
}
DigestCallback digestCallback = new DigestCallback();
HmacCallback hmacCallback = new HmacCallback();
executeCallbacks(digestCallback, hmacCallback);
byte[] salt = generateSalt();
try {
ScramUtils.NewPasswordStringData data =
ScramUtils.byteArrayToStringData(ScramUtils.newPassword(plainTextPassword, salt, 4096,
digestCallback.getDigest(),
hmacCallback.getHmac()));
return new UserData(data.salt, data.iterations, data.serverKey, data.storedKey);
} catch (ScramException e) {
throw new LoginException();
}
}
private static byte[] generateSalt() {
byte[] salt = new byte[32];
RANDOM_GENERATOR.nextBytes(salt);
return salt;
}
private void executeCallbacks(Callback... callbacks) throws LoginException {
try {
callbackHandler.handle(callbacks);
} catch (UnsupportedCallbackException | IOException e) {
throw new LoginException();
}
}
@Override
public boolean commit() throws LoginException {
if (userData == null) {
throw new LoginException();
}
subject.getPublicCredentials().add(userData);
Set<UserPrincipal> authenticatedUsers = subject.getPrincipals(UserPrincipal.class);
UserPrincipal principal = new UserPrincipal(user);
principals.add(principal);
authenticatedUsers.add(principal);
for (UserPrincipal userPrincipal : authenticatedUsers) {
Set<String> matchedRoles = roles.get(userPrincipal.getName());
if (matchedRoles != null) {
for (String entry : matchedRoles) {
principals.add(new RolePrincipal(entry));
}
}
}
subject.getPrincipals().addAll(principals);
return true;
}
@Override
public boolean abort() throws LoginException {
return true;
}
@Override
public boolean logout() throws LoginException {
subject.getPrincipals().removeAll(principals);
principals.clear();
subject.getPublicCredentials().remove(userData);
userData = null;
return true;
}
/**
* Main method that could be used to encrypt given credentials for use in properties files
* @param args username password type [iterations]
* @throws GeneralSecurityException if any security mechanism is not available on this JVM
* @throws ScramException if invalid data is supplied
* @throws StringPrepError if username can't be encoded according to SASL StringPrep
* @throws IOException if writing as properties failed
*/
public static void main(String[] args) throws GeneralSecurityException, ScramException, StringPrepError,
IOException {
if (args.length < 2) {
System.out.println("Usage: " + SCRAMPropertiesLoginModule.class.getSimpleName() +
" <username> <password> [<iterations>]");
System.out.println("\ttype: " + getSupportedTypes());
System.out.println("\titerations desired number of iteration (min value: " + MIN_ITERATIONS + ")");
return;
}
String username = args[0];
String password = args[1];
Properties properties = new Properties();
String encodedUser = StringPrep.prepAsQueryString(username);
for (SCRAM scram : SCRAM.values()) {
MessageDigest digest = MessageDigest.getInstance(scram.getDigest());
Mac hmac = Mac.getInstance(scram.getHmac());
byte[] salt = generateSalt();
int iterations;
if (args.length > 2) {
iterations = Integer.parseInt(args[2]);
if (iterations < MIN_ITERATIONS) {
throw new IllegalArgumentException("minimum of " + MIN_ITERATIONS + " required!");
}
} else {
iterations = MIN_ITERATIONS;
}
ScramUtils.NewPasswordStringData data =
ScramUtils.byteArrayToStringData(ScramUtils.newPassword(password, salt, iterations, digest, hmac));
String encodedPassword = PasswordMaskingUtil.wrap(data.salt + SEPARATOR_PARAMETER + data.iterations +
SEPARATOR_PARAMETER + data.serverKey + SEPARATOR_PARAMETER + data.storedKey);
properties.setProperty(encodedUser + SEPARATOR_MECHANISM + scram.name(), encodedPassword);
}
properties.store(System.out,
"Insert the lines stating with '" + encodedUser + "' into the desired user properties file");
}
private static SCRAM getTypeByString(String type) {
SCRAM scram = Arrays.stream(SCRAM.values())
.filter(v -> v.getName().equals(type))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("unkown type " + type +
", supported ones are " + getSupportedTypes()));
return scram;
}
private static String getSupportedTypes() {
return String.join(", ", Arrays.stream(SCRAM.values()).map(SCRAM::getName).toArray(String[]::new));
}
}

View File

@ -0,0 +1,56 @@
/*
* 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.activemq.artemis.spi.core.security.scram;
/**
* Defines sets of known SCRAM types with methods to fetch matching digest and hmac names
*/
public enum SCRAM {
// ordered by precedence
SHA512,
SHA256;
public String getName() {
switch (this) {
case SHA256:
return "SCRAM-SHA-256";
case SHA512:
return "SCRAM-SHA-512";
}
throw new UnsupportedOperationException();
}
public String getDigest() {
switch (this) {
case SHA256:
return "SHA-256";
case SHA512:
return "SHA-512";
}
throw new UnsupportedOperationException();
}
public String getHmac() {
switch (this) {
case SHA256:
return "HmacSHA256";
case SHA512:
return "HmacSHA512";
}
throw new UnsupportedOperationException();
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright 2016 Ognyan Bankov
* <p>
* All rights reserved. Licensed 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.activemq.artemis.spi.core.security.scram;
import java.security.GeneralSecurityException;
/**
* Indicates error while processing SCRAM sequence
*/
public class ScramException extends Exception {
/**
* Creates new ScramException
* @param message Exception message
*/
public ScramException(String message) {
super(message);
}
public ScramException(String message, GeneralSecurityException e) {
super(message, e);
}
/**
* Creates new ScramException
* @param cause Throwable
*/
public ScramException(Throwable cause) {
super(cause);
}
}

View File

@ -0,0 +1,264 @@
/*
* Copyright 2016 Ognyan Bankov
* <p>
* All rights reserved. Licensed 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.activemq.artemis.spi.core.security.scram;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
/**
* Provides static methods for working with SCRAM/SASL
*/
public class ScramUtils {
private static final byte[] INT_1 = new byte[] {0, 0, 0, 1};
private ScramUtils() {
throw new AssertionError("non-instantiable utility class");
}
/**
* Generates salted password.
* @param password Clear form password, i.e. what user typed
* @param salt Salt to be used
* @param iterationsCount Iterations for 'salting'
* @param mac HMAC to be used
* @return salted password
* @throws ScramException
*/
public static byte[] generateSaltedPassword(final String password, byte[] salt, int iterationsCount,
Mac mac) throws ScramException {
SecretKeySpec key = new SecretKeySpec(password.getBytes(StandardCharsets.US_ASCII), mac.getAlgorithm());
try {
mac.init(key);
} catch (InvalidKeyException e) {
throw new ScramException("Incompatible key", e);
}
mac.update(salt);
mac.update(INT_1);
byte[] result = mac.doFinal();
byte[] previous = null;
for (int i = 1; i < iterationsCount; i++) {
mac.update(previous != null ? previous : result);
previous = mac.doFinal();
for (int x = 0; x < result.length; x++) {
result[x] ^= previous[x];
}
}
return result;
}
/**
* Creates HMAC
* @param keyBytes key
* @param hmacName HMAC name
* @return Mac
* @throws InvalidKeyException if internal error occur while working with SecretKeySpec
* @throws NoSuchAlgorithmException if hmacName is not supported by the java
*/
public static Mac createHmac(final byte[] keyBytes, String hmacName) throws NoSuchAlgorithmException,
InvalidKeyException {
Mac mac = Mac.getInstance(hmacName);
SecretKeySpec key = new SecretKeySpec(keyBytes, hmacName);
mac.init(key);
return mac;
}
/**
* Computes HMAC byte array for given string
* @param key key
* @param hmacName HMAC name
* @param string string for which HMAC will be computed
* @return computed HMAC
* @throws InvalidKeyException if internal error occur while working with SecretKeySpec
* @throws NoSuchAlgorithmException if hmacName is not supported by the java
*/
public static byte[] computeHmac(final byte[] key, String hmacName, final String string) throws InvalidKeyException,
NoSuchAlgorithmException {
Mac mac = createHmac(key, hmacName);
mac.update(string.getBytes(StandardCharsets.US_ASCII));
return mac.doFinal();
}
public static byte[] computeHmac(final byte[] key, Mac hmac, final String string) throws ScramException {
try {
hmac.init(new SecretKeySpec(key, hmac.getAlgorithm()));
} catch (InvalidKeyException e) {
throw new ScramException("invalid key", e);
}
hmac.update(string.getBytes(StandardCharsets.US_ASCII));
return hmac.doFinal();
}
/**
* Checks if string is null or empty
* @param string String to be tested
* @return true if the string is null or empty, false otherwise
*/
public static boolean isNullOrEmpty(String string) {
return string == null || string.length() == 0; // string.isEmpty() in Java 6
}
/**
* Computes the data associated with new password like salted password, keys, etc
* <p>
* This method is supposed to be used by a server when user provides new clear form password. We
* don't want to save it that way so we generate salted password and store it along with other
* data required by the SCRAM mechanism
* @param passwordClearText Clear form password, i.e. as provided by the user
* @param salt Salt to be used
* @param iterations Iterations for 'salting'
* @param mac HMAC name to be used
* @param messageDigest Digest name to be used
* @return new password data while working with SecretKeySpec
* @throws ScramException
*/
public static NewPasswordByteArrayData newPassword(String passwordClearText, byte[] salt, int iterations,
MessageDigest messageDigest, Mac mac) throws ScramException {
byte[] saltedPassword = ScramUtils.generateSaltedPassword(passwordClearText, salt, iterations, mac);
byte[] clientKey = ScramUtils.computeHmac(saltedPassword, mac, "Client Key");
byte[] storedKey = messageDigest.digest(clientKey);
byte[] serverKey = ScramUtils.computeHmac(saltedPassword, mac, "Server Key");
return new NewPasswordByteArrayData(saltedPassword, salt, clientKey, storedKey, serverKey, iterations);
}
/**
* Transforms NewPasswordByteArrayData into NewPasswordStringData into database friendly (string)
* representation Uses Base64 to encode the byte arrays into strings
* @param ba Byte array data
* @return String data
*/
public static NewPasswordStringData byteArrayToStringData(NewPasswordByteArrayData ba) {
return new NewPasswordStringData(Base64.getEncoder().encodeToString(ba.saltedPassword),
Base64.getEncoder().encodeToString(ba.salt),
Base64.getEncoder().encodeToString(ba.clientKey),
Base64.getEncoder().encodeToString(ba.storedKey),
Base64.getEncoder().encodeToString(ba.serverKey), ba.iterations);
}
/**
* New password data in database friendly format, i.e. Base64 encoded strings
*/
@SuppressWarnings("unused")
public static class NewPasswordStringData {
/**
* Salted password
*/
public final String saltedPassword;
/**
* Used salt
*/
public final String salt;
/**
* Client key
*/
public final String clientKey;
/**
* Stored key
*/
public final String storedKey;
/**
* Server key
*/
public final String serverKey;
/**
* Iterations for slating
*/
public final int iterations;
/**
* Creates new NewPasswordStringData
* @param saltedPassword Salted password
* @param salt Used salt
* @param clientKey Client key
* @param storedKey Stored key
* @param serverKey Server key
* @param iterations Iterations for slating
*/
public NewPasswordStringData(String saltedPassword, String salt, String clientKey, String storedKey,
String serverKey, int iterations) {
this.saltedPassword = saltedPassword;
this.salt = salt;
this.clientKey = clientKey;
this.storedKey = storedKey;
this.serverKey = serverKey;
this.iterations = iterations;
}
}
/**
* New password data in byte array format
*/
@SuppressWarnings("unused")
public static class NewPasswordByteArrayData {
/**
* Salted password
*/
public final byte[] saltedPassword;
/**
* Used salt
*/
public final byte[] salt;
/**
* Client key
*/
public final byte[] clientKey;
/**
* Stored key
*/
public final byte[] storedKey;
/**
* Server key
*/
public final byte[] serverKey;
/**
* Iterations for slating
*/
public final int iterations;
/**
* Creates new NewPasswordByteArrayData
* @param saltedPassword Salted password
* @param salt Used salt
* @param clientKey Client key
* @param storedKey Stored key
* @param serverKey Server key
* @param iterations Iterations for slating
*/
public NewPasswordByteArrayData(byte[] saltedPassword, byte[] salt, byte[] clientKey, byte[] storedKey,
byte[] serverKey, int iterations) {
this.saltedPassword = saltedPassword;
this.salt = salt;
this.clientKey = clientKey;
this.storedKey = storedKey;
this.serverKey = serverKey;
this.iterations = iterations;
}
}
}

View File

@ -0,0 +1,53 @@
/*
* Copyright 2016 Ognyan Bankov
* <p>
* All rights reserved. Licensed 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.activemq.artemis.spi.core.security.scram;
/**
* Wrapper for user data needed for the SCRAM authentication
*/
public class UserData {
/**
* Salt
*/
public final String salt;
/**
* Iterations used to salt the password
*/
public final int iterations;
/**
* Server key
*/
public final String serverKey;
/**
* Stored key
*/
public final String storedKey;
/**
* Creates new UserData
* @param salt Salt
* @param iterations Iterations for salting
* @param serverKey Server key
* @param storedKey Stored key
*/
public UserData(String salt, int iterations, String serverKey, String storedKey) {
this.salt = salt;
this.iterations = iterations;
this.serverKey = serverKey;
this.storedKey = storedKey;
}
}

View File

@ -16,17 +16,18 @@
*/
package org.apache.activemq.artemis.spi.core.security.jaas;
import org.junit.Test;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.IOException;
import java.security.Principal;
import javax.security.auth.Subject;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import java.io.IOException;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class Krb5LoginModuleTest {
@ -52,7 +53,7 @@ public class Krb5LoginModuleTest {
underTest.initialize(subject, new CallbackHandler() {
@Override
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
((Krb5Callback) callbacks[0]).setPeerPrincipal(new UserPrincipal("A"));
((PrincipalsCallback) callbacks[0]).setPeerPrincipals(new Principal[] {new UserPrincipal("A")});
}
}, null, null);

View File

@ -51,6 +51,7 @@ under the License.
<module>proton-clustered-cpp</module>
<module>queue</module>
<module>proton-ruby</module>
<module>sasl-scram</module>
</modules>
</profile>
<profile>

View File

@ -0,0 +1,43 @@
<?xml version='1.0'?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.activemq.examples.amqp</groupId>
<artifactId>amqp</artifactId>
<version>2.18.0-SNAPSHOT</version>
</parent>
<properties>
<activemq.basedir>${project.basedir}/../../../..</activemq.basedir>
</properties>
<artifactId>sasl-scram</artifactId>
<packaging>pom</packaging>
<name>ActiveMQ Artemis SASL-SCRAM Example</name>
<modules>
<module>sasl-client</module>
<module>sasl-server</module>
</modules>
</project>

View File

@ -0,0 +1,49 @@
<?xml version='1.0'?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.activemq.examples.amqp</groupId>
<artifactId>sasl-scram</artifactId>
<version>2.18.0-SNAPSHOT</version>
</parent>
<artifactId>sasl-scram-client</artifactId>
<packaging>jar</packaging>
<name>ActiveMQ Artemis SASL-SCRAM-Client Example</name>
<properties>
<activemq.basedir>${project.basedir}/../../../../..</activemq.basedir>
<artemis-version>${project.version}</artemis-version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.qpid</groupId>
<artifactId>qpid-jms-client</artifactId>
<version>${qpid.jms.version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,3 @@
# Artemis SASL-SCRAM Server and Client Example
demonstrate the usage of SASL-SCRAM authentication with ActiveMQ Artemis

View File

@ -0,0 +1,48 @@
/*
* <p>
* All rights reserved. Licensed 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.activemq.artemis.jms.example;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.qpid.jms.JmsConnectionFactory;
public class QPIDClient {
public static void main(String[] args) throws JMSException {
sendReceive("SCRAM-SHA-1", "hello", "ogre1234");
sendReceive("SCRAM-SHA-256", "test", "test");
}
private static void sendReceive(String method, String username, String password) throws JMSException {
ConnectionFactory connectionFactory =
new JmsConnectionFactory("amqp://localhost:5672?amqp.saslMechanisms=" + method);
try (Connection connection = connectionFactory.createConnection(username, password)) {
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue("exampleQueue");
MessageProducer sender = session.createProducer(queue);
sender.send(session.createTextMessage("Hello " + method));
connection.start();
MessageConsumer consumer = session.createConsumer(queue);
TextMessage m = (TextMessage) consumer.receive(5000);
System.out.println("message = " + m.getText());
}
}
}

View File

@ -0,0 +1,54 @@
<?xml version='1.0'?>
<!--
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.
-->
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.apache.activemq.examples.amqp</groupId>
<artifactId>sasl-scram</artifactId>
<version>2.18.0-SNAPSHOT</version>
</parent>
<artifactId>sasl-scram-server</artifactId>
<packaging>jar</packaging>
<name>ActiveMQ Artemis SASL-SCRAM-Server Example</name>
<properties>
<activemq.basedir>${project.basedir}/../../../../..</activemq.basedir>
<artemis-version>${project.version}</artemis-version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-server</artifactId>
<version>${artemis-version}</version>
</dependency>
<dependency>
<groupId>org.apache.activemq</groupId>
<artifactId>artemis-amqp-protocol</artifactId>
<version>${artemis-version}</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,3 @@
# Artemis SASL-SCRAM Server and Client Example
demonstrate the usage of SASL-SCRAM authentication with ActiveMQ Artemis

View File

@ -0,0 +1,35 @@
/*
* <p>
* All rights reserved. Licensed 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.activemq.artemis.jms.example;
import java.io.File;
import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ;
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
public class TestServer {
public static void main(String[] args) throws Exception {
File configFolder = new File(args.length > 0 ? args[0] : "src/main/resources/").getAbsoluteFile();
System.setProperty("java.security.auth.login.config", new File(configFolder, "login.conf").getAbsolutePath());
EmbeddedActiveMQ embedded = new EmbeddedActiveMQ();
embedded.setSecurityManager(new ActiveMQJAASSecurityManager("artemis"));
embedded.setConfigResourcePath(new File(configFolder, "broker.xml").getAbsoluteFile().toURI().toASCIIString());
embedded.start();
while (true) {
// intentional empty
}
}
}

View File

@ -0,0 +1,18 @@
## ---------------------------------------------------------------------------
## 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.
## ---------------------------------------------------------------------------
user=hello,test
admin=test

View File

@ -0,0 +1,24 @@
## ---------------------------------------------------------------------------
## 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.
## ---------------------------------------------------------------------------
##
# Example for an encoded username/password, encoded forms can be generated with java org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule <username> <password> [<iterations>]
test|SHA512 = ENC(7TilOEFipzE4KNkDUTlfnuMkYE1yveyXmK6iBx8/fnE=:4096:yPl/n8eZQEyVmkhuYvrgZCchEpO+a9QiGLXwJfqBWOIfTxMX5TkoHp5eYGABc68cUvoynqCnoqRLDPac+H1urg==:eX5X39hbChbXz00TCkMpmsHqsJTiMGCwamty6yjUS0M+HoE/SLtd2MYY1Shyn+5mu30qFsbXz0WlRA+dZ3Lv3A==)
test|SHA256 = ENC(yNekJSAvbunYIIHKni32oXgg7uCSUZSzvgNq3pLL3so=:4096:45p4iB+tgMB2b2FM6MmuzyTF63QOfQroQLwNXxhCZ48=:PXUabvM/90DWQsl/p9Cp7wYlavCTPJZnzdU9PFUuiXc=)
test|SHA1 = ENC(ehArM+Qzko2eua0hMq0o+NQ9BaTTf4q8xY0tzfy2Zvw=:4096:LvpLr4ezL4ICxeiXAkXEVH9EhO0=:gLELi8NpLVorxXbPIIbVZF/oqh8=)
# Example for a plain username/password, don't use this on public servers!
hello = ogre1234

View File

@ -0,0 +1,50 @@
<?xml version='1.0'?>
<!--
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.
-->
<configuration
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="urn:activemq"
xsi:schemaLocation="urn:activemq/schema/artemis-server.xsd">
<core xmlns="urn:activemq:core">
<persistence-enabled>false</persistence-enabled>
<security-enabled>true</security-enabled>
<acceptors>
<acceptor name="amqp">tcp://localhost:5672?protocols=AMQP;saslMechanisms=SCRAM-SHA-256,SCRAM-SHA-1;saslLoginConfigScope=amqp-sasl-scram
</acceptor>
</acceptors>
<security-settings>
<security-setting match="#">
<permission type="createAddress" roles="user" />
<permission type="createDurableQueue"
roles="user" />
<permission type="deleteDurableQueue"
roles="user" />
<permission type="createNonDurableQueue"
roles="user" />
<permission type="deleteNonDurableQueue"
roles="user" />
<permission type="consume" roles="user" />
<permission type="send" roles="user" />
</security-setting>
</security-settings>
</core>
</configuration>

View File

@ -0,0 +1,29 @@
/*
* 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.
*/
amqp-sasl-scram {
org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule required
debug=false
org.apache.activemq.jaas.properties.user="artemis-users.properties"
org.apache.activemq.jaas.properties.role="artemis-roles.properties";
};
artemis {
org.apache.activemq.artemis.spi.core.security.jaas.SCRAMLoginModule required
;
};

2
tests/integration-tests/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/.apt_generated/
/.apt_generated_tests/

View File

@ -17,13 +17,24 @@
package org.apache.activemq.artemis.tests.integration.amqp.connect;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import javax.crypto.Mac;
import javax.security.auth.login.LoginException;
import org.apache.activemq.artemis.core.config.amqpBrokerConnectivity.AMQPBrokerConnectConfiguration;
import org.apache.activemq.artemis.core.server.ActiveMQServer;
import org.apache.activemq.artemis.protocol.amqp.sasl.SASLResult;
import org.apache.activemq.artemis.protocol.amqp.sasl.scram.SCRAMServerSASL;
import org.apache.activemq.artemis.spi.core.security.scram.SCRAM;
import org.apache.activemq.artemis.spi.core.security.scram.ScramUtils;
import org.apache.activemq.artemis.spi.core.security.scram.UserData;
import org.apache.activemq.artemis.tests.integration.amqp.AmqpClientTestSupport;
import org.apache.qpid.proton.engine.Sasl;
import org.apache.qpid.proton.engine.Sasl.SaslOutcome;
@ -107,7 +118,7 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
// No user or pass given, it will have to select ANONYMOUS even though PLAIN also offered
AMQPBrokerConnectConfiguration amqpConnection =
new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://localhost:" + mockServer.actualPort());
new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://localhost:" + mockServer.actualPort());
amqpConnection.setReconnectAttempts(0);// No reconnects
server.getConfiguration().addAMQPConnection(amqpConnection);
@ -135,7 +146,8 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
});
// User and pass are given, it will select PLAIN
AMQPBrokerConnectConfiguration amqpConnection = new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://localhost:" + mockServer.actualPort());
AMQPBrokerConnectConfiguration amqpConnection =
new AMQPBrokerConnectConfiguration("testSimpleConnect", "tcp://localhost:" + mockServer.actualPort());
amqpConnection.setReconnectAttempts(0);// No reconnects
amqpConnection.setUser(USER);
amqpConnection.setPassword(PASSWD);
@ -151,6 +163,36 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
assertArrayEquals(expectedPlainInitialResponse(USER, PASSWD), authenticator.getInitialResponse());
}
@Test(timeout = 200000)
public void testConnectsWithSCRAM() throws Exception {
CountDownLatch serverConnectionOpen = new CountDownLatch(1);
SCRAMTestAuthenticator authenticator = new SCRAMTestAuthenticator(SCRAM.SHA512);
mockServer = new MockServer(vertx, () -> authenticator, serverConnection -> {
serverConnection.openHandler(serverSender -> {
serverConnectionOpen.countDown();
serverConnection.closeHandler(x -> serverConnection.close());
serverConnection.open();
});
});
AMQPBrokerConnectConfiguration amqpConnection =
new AMQPBrokerConnectConfiguration("testSScramConnect", "tcp://localhost:" + mockServer.actualPort());
amqpConnection.setReconnectAttempts(0);// No reconnects
amqpConnection.setUser(USER);
amqpConnection.setPassword(PASSWD);
server.getConfiguration().addAMQPConnection(amqpConnection);
server.start();
boolean awaitConnectionOpen = serverConnectionOpen.await(10, TimeUnit.SECONDS);
assertTrue("Broker did not open connection in alotted time", awaitConnectionOpen);
assertEquals(SCRAM.SHA512.getName(), authenticator.chosenMech);
assertTrue(authenticator.succeeded());
}
@Test(timeout = 20000)
public void testConnectsWithExternal() throws Exception {
doConnectWithExternalTestImpl(true);
@ -161,10 +203,13 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
doConnectWithExternalTestImpl(false);
}
private void doConnectWithExternalTestImpl(boolean requireClientCert) throws ExecutionException, InterruptedException, Exception {
private void doConnectWithExternalTestImpl(boolean requireClientCert) throws ExecutionException,
InterruptedException, Exception {
CountDownLatch serverConnectionOpen = new CountDownLatch(1);
// The test server always offers EXTERNAL, i.e sometimes mistakenly, to verify that the broker only selects it when it actually
// has a client-cert. Real servers shouldnt actually offer the mechanism to a client that didnt have to provide a cert.
// The test server always offers EXTERNAL, i.e sometimes mistakenly, to verify that the broker
// only selects it when it actually
// has a client-cert. Real servers shouldnt actually offer the mechanism to a client that
// didnt have to provide a cert.
TestAuthenticator authenticator = new TestAuthenticator(true, EXTERNAL, PLAIN);
final String keyStorePath = this.getClass().getClassLoader().getResource(SERVER_KEYSTORE_NAME).getFile();
@ -191,14 +236,17 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
});
String amqpServerConnectionURI = "tcp://localhost:" + mockServer.actualPort() +
"?sslEnabled=true;trustStorePath=" + TRUSTSTORE_NAME + ";trustStorePassword=" + TRUSTSTORE_PASSWORD;
"?sslEnabled=true;trustStorePath=" + TRUSTSTORE_NAME + ";trustStorePassword=" + TRUSTSTORE_PASSWORD;
if (requireClientCert) {
amqpServerConnectionURI += ";keyStorePath=" + CLIENT_KEYSTORE_NAME + ";keyStorePassword=" + CLIENT_KEYSTORE_PASSWORD;
amqpServerConnectionURI +=
";keyStorePath=" + CLIENT_KEYSTORE_NAME + ";keyStorePassword=" + CLIENT_KEYSTORE_PASSWORD;
}
AMQPBrokerConnectConfiguration amqpConnection = new AMQPBrokerConnectConfiguration("testSimpleConnect", amqpServerConnectionURI);
AMQPBrokerConnectConfiguration amqpConnection =
new AMQPBrokerConnectConfiguration("testSimpleConnect", amqpServerConnectionURI);
amqpConnection.setReconnectAttempts(0);// No reconnects
amqpConnection.setUser(USER); // Wont matter if EXTERNAL is offered and a client-certificate is provided, but will otherwise.
amqpConnection.setUser(USER); // Wont matter if EXTERNAL is offered and a client-certificate
// is provided, but will otherwise.
amqpConnection.setPassword(PASSWD);
server.getConfiguration().addAMQPConnection(amqpConnection);
@ -236,8 +284,8 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
private static final class TestAuthenticator implements ProtonSaslAuthenticator {
private Sasl sasl;
private boolean succeed;
private String[] offeredMechs;
private final boolean succeed;
private final String[] offeredMechs;
String chosenMech = null;
byte[] initialResponse = null;
boolean done = false;
@ -268,7 +316,6 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
initialResponse = new byte[sasl.pending()];
sasl.recv(initialResponse, 0, initialResponse.length);
if (succeed) {
sasl.done(SaslOutcome.PN_SASL_OK);
} else {
@ -296,4 +343,103 @@ public class AMQPConnectSaslTest extends AmqpClientTestSupport {
}
}
private static final class SCRAMTestAuthenticator implements ProtonSaslAuthenticator {
private final SCRAM mech;
private Sasl sasl;
private TestSCRAMServerSASL serverSASL;
private String chosenMech;
SCRAMTestAuthenticator(SCRAM mech) {
this.mech = mech;
}
@Override
public void init(NetSocket socket, ProtonConnection protonConnection, Transport transport) {
this.sasl = transport.sasl();
sasl.server();
sasl.allowSkip(false);
sasl.setMechanisms(mech.getName(), PLAIN, ANONYMOUS);
try {
serverSASL = new TestSCRAMServerSASL(mech);
} catch (NoSuchAlgorithmException e) {
throw new AssertionError(e);
}
}
@Override
public void process(Handler<Boolean> completionHandler) {
String[] remoteMechanisms = sasl.getRemoteMechanisms();
int pending = sasl.pending();
if (remoteMechanisms.length == 0 || pending == 0) {
completionHandler.handle(false);
return;
}
chosenMech = remoteMechanisms[0];
byte[] msg = new byte[pending];
sasl.recv(msg, 0, msg.length);
byte[] result = serverSASL.processSASL(msg);
if (result != null) {
sasl.send(result, 0, result.length);
}
boolean ended = serverSASL.isEnded();
if (ended) {
if (succeeded()) {
sasl.done(SaslOutcome.PN_SASL_OK);
} else {
sasl.done(SaslOutcome.PN_SASL_AUTH);
}
completionHandler.handle(true);
} else {
completionHandler.handle(false);
}
}
@Override
public boolean succeeded() {
SASLResult result = serverSASL.result();
return result != null && result.isSuccess() && serverSASL.e == null;
}
}
private static final class TestSCRAMServerSASL extends SCRAMServerSASL {
private Exception e;
TestSCRAMServerSASL(SCRAM mechanism) throws NoSuchAlgorithmException {
super(mechanism);
}
@Override
public void done() {
// nothing to do
}
@Override
protected UserData aquireUserData(String userName) throws LoginException {
if (!USER.equals(userName)) {
throw new LoginException("invalid username");
}
byte[] salt = new byte[32];
new SecureRandom().nextBytes(salt);
try {
MessageDigest digest = MessageDigest.getInstance(mechanism.getDigest());
Mac hmac = Mac.getInstance(mechanism.getHmac());
ScramUtils.NewPasswordStringData data =
ScramUtils.byteArrayToStringData(ScramUtils.newPassword(PASSWD, salt, 4096, digest, hmac));
return new UserData(data.salt, data.iterations, data.serverKey, data.storedKey);
} catch (Exception e) {
throw new LoginException(e.getMessage());
}
}
@Override
protected void failed(Exception e) {
this.e = e;
}
}
}

View File

@ -0,0 +1,117 @@
/**
* 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
* <p>
* http://www.apache.org/licenses/LICENSE-2.0
* <p>
* 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.activemq.artemis.tests.integration.amqp.sasl;
import static org.junit.Assert.assertEquals;
import java.io.File;
import javax.jms.Connection;
import javax.jms.ConnectionFactory;
import javax.jms.JMSException;
import javax.jms.MessageConsumer;
import javax.jms.MessageProducer;
import javax.jms.Queue;
import javax.jms.Session;
import javax.jms.TextMessage;
import org.apache.activemq.artemis.core.server.embedded.EmbeddedActiveMQ;
import org.apache.activemq.artemis.spi.core.security.ActiveMQJAASSecurityManager;
import org.apache.qpid.jms.JmsConnectionFactory;
import org.apache.qpid.jms.exceptions.JMSSecuritySaslException;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
/**
* This test SASL-SCRAM Support
*/
public class SaslScramTest {
private static EmbeddedActiveMQ BROKER;
@BeforeClass
public static void startBroker() throws Exception {
String loginConfPath = new File(SaslScramTest.class.getResource("/login.config").toURI()).getAbsolutePath();
System.out.println(loginConfPath);
System.setProperty("java.security.auth.login.config", loginConfPath);
BROKER = new EmbeddedActiveMQ();
BROKER.setConfigResourcePath(SaslScramTest.class.getResource("/broker-saslscram.xml").toExternalForm());
BROKER.setSecurityManager(new ActiveMQJAASSecurityManager("artemis-sasl-scram"));
BROKER.start();
}
@AfterClass
public static void shutdownBroker() throws Exception {
BROKER.stop();
}
/**
* Checks if a user with plain text password can login using all mechanisms
* @throws JMSException should not happen
*/
@Test
public void testUnencryptedWorksWithAllMechanism() throws JMSException {
sendRcv("SCRAM-SHA-1", "hello", "ogre1234");
sendRcv("SCRAM-SHA-256", "hello", "ogre1234");
}
/**
* Checks that a user that has encrypted passwords for all mechanism can login with any of them
* @throws JMSException should not happen
*/
@Test
public void testEncryptedWorksWithAllMechanism() throws JMSException {
sendRcv("SCRAM-SHA-1", "multi", "worksforall");
sendRcv("SCRAM-SHA-256", "multi", "worksforall");
}
/**
* Checks that a user that is only stored with one explicit mechanism can't use another mechanism
* @throws JMSException is expected
*/
@Test(expected = JMSSecuritySaslException.class)
public void testEncryptedWorksOnlyWithMechanism() throws JMSException {
sendRcv("SCRAM-SHA-1", "test", "test");
}
/**
* Checks that a user that is only stored with one explicit mechanism can login with this
* mechanism
* @throws JMSException should not happen
*/
@Test
public void testEncryptedWorksWithMechanism() throws JMSException {
sendRcv("SCRAM-SHA-256", "test", "test");
}
private void sendRcv(String method, String username, String password) throws JMSException {
ConnectionFactory connectionFactory =
new JmsConnectionFactory("amqp://localhost:5672?amqp.saslMechanisms=" + method);
try (Connection connection = connectionFactory.createConnection(username, password)) {
Session session = connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
Queue queue = session.createQueue("exampleQueue");
MessageProducer sender = session.createProducer(queue);
String text = "Hello " + method;
sender.send(session.createTextMessage(text));
connection.start();
MessageConsumer consumer = session.createConsumer(queue);
TextMessage m = (TextMessage) consumer.receive(5000);
assertEquals(text, m.getText());
}
}
}

View File

@ -0,0 +1,18 @@
## ---------------------------------------------------------------------------
## 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.
## ---------------------------------------------------------------------------
user=hello,test,multi
admin=multi

View File

@ -0,0 +1,26 @@
## ---------------------------------------------------------------------------
## 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.
## ---------------------------------------------------------------------------
# Example for an encoded username/password, encoded forms can be generated with java org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule <username> <password> [<iterations>]
multi|SHA256 = ENC(o3ljCITL4Cw6pu+fxvz4k68F8jQuZAITcRNpy2THufw=:4096:Niuc0/lWg/YQztHqJCJ5SodyxbWPtGj6zp/HHPqSDBY=:O1YL/w08fvuTvqctbHrr4TxpzKso+NCdqt4Amqp7r0k=)
multi|SHA1 = ENC(cJyfpU4wgmoSz1GVc39+CooXY2jIv2ILe7486+l9vbg=:4096:sMCix9TeOvmo2eb4xbCjQt4navs=:fSKdLYAgdx36RMjjMSn1dZ7IpY8=)
# Example for a plain username/password, don't use this on public servers!
hello = ogre1234
# just for unit-test purpose!
test = ENC(yNekJSAvbunYIIHKni32oXgg7uCSUZSzvgNq3pLL3so=:4096:45p4iB+tgMB2b2FM6MmuzyTF63QOfQroQLwNXxhCZ48=:PXUabvM/90DWQsl/p9Cp7wYlavCTPJZnzdU9PFUuiXc=)

View File

@ -0,0 +1,50 @@
<?xml version='1.0'?>
<!--
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.
-->
<configuration
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="urn:activemq"
xsi:schemaLocation="urn:activemq/schema/artemis-server.xsd">
<core xmlns="urn:activemq:core">
<persistence-enabled>false</persistence-enabled>
<security-enabled>true</security-enabled>
<acceptors>
<acceptor name="amqp">tcp://localhost:5672?protocols=AMQP;saslMechanisms=SCRAM-SHA-256,SCRAM-SHA-1;saslLoginConfigScope=amqp-sasl-scram
</acceptor>
</acceptors>
<security-settings>
<security-setting match="#">
<permission type="createAddress" roles="user" />
<permission type="createDurableQueue"
roles="user" />
<permission type="deleteDurableQueue"
roles="user" />
<permission type="createNonDurableQueue"
roles="user" />
<permission type="deleteNonDurableQueue"
roles="user" />
<permission type="consume" roles="user" />
<permission type="send" roles="user" />
</security-setting>
</security-settings>
</core>
</configuration>

View File

@ -319,3 +319,15 @@ amqp-jms-client {
com.sun.security.auth.module.Krb5LoginModule required
useKeyTab=true;
};
amqp-sasl-scram {
org.apache.activemq.artemis.spi.core.security.jaas.SCRAMPropertiesLoginModule required
debug=false
org.apache.activemq.jaas.properties.user="artemis-scram-users.properties"
org.apache.activemq.jaas.properties.role="artemis-scram-roles.properties";
};
artemis-sasl-scram {
org.apache.activemq.artemis.spi.core.security.jaas.SCRAMLoginModule required
;
};