Python STOMP client(从 4.1.20 版开始)使用 SSLContext 来处理其密钥对/证书,因此没有理由为客户端生成 Java KeyStore。
考虑到这一点,让我们完成设置 ApacheMQ 以支持 SSL 包装的 STOMP 连接的整个过程。以下过程已在 ApacheMQ 5.15.4 上进行了测试。我们通过在代理和客户端之间手动移动自签名证书来明确设置双向信任;也可以使用证书颁发机构,但如何做到这一点是另一个问题。
创建客户端证书
如上所述,在 Python 方面,KeyStore 几乎没有用处,由于 SSLContext 需要 PEM 编码的证书,我们不妨手动创建密钥对和证书(即使用 @987654325 @)。首先,在客户端机器上,让我们创建一个 4096 位的 RSA 密钥:
openssl genrsa -out client.key 4096
使用它,将公钥部分变成证书,并用密钥本身对其进行签名;由于我们将手动将证书移动到代理,因此自签名证书不是问题:
openssl req -new -out client.csr -key client.key
openssl x509 -req -days 365 -in client.csr -signkey client.key -out client.pem
rm client.csr
STOMP 客户端将需要签名证书client.pem 和私钥client.key,而代理只需要证书。
创建代理证书
在代理上,我们可以按照 Apache 指南的第一部分,使用 Java keytool 为服务器创建一个带有密钥的 KeyStore:
keytool -genkeypair -alias broker -keyalg RSA -keysize 4096 -keystore broker.ks
当提示输入“名字和姓氏”时,提供服务器的主机名,在我们的示例中,我们将简单地取为localhost;如果代理和客户端在不同的服务器上运行,请确保将其设置为 Python 客户端最终用于识别代理的任何内容:
What is your first and last name?
[Unknown]: localhost
所有其他输入值都可以保留为“未知”。
在一天结束时,我们只希望允许来自具有我们知道的证书的客户端连接到代理,因此此时将上面生成的client.pem 复制到代理并通过以下方式将其添加到信任库中
keytool -import -alias client -keystore broker.ts -file client.pem
如果代理允许来自任何客户端的连接,则可以跳过最后一步。
设置 ApacheMQ
默认情况下,所有通过 STOMP 的连接(实际上是所有连接)都是明文连接,为了启用 SSL 上的 STOMP 连接,请将以下 <transportConnector /> 添加到 conf/apachemq.xml:
<transportConnectors>
<transportConnector name="stomp+ssl" uri="stomp+nio+ssl://0.0.0.0:61613?transport.enabledProtocols=TLSv1.2&needClientAuth=true" />
</transportConnectors>
确保删除任何现有的纯文本连接器,例如默认的 STOMP 连接器,否则客户端将能够简单地使用这些连接器并绕过 SSL 要求。另请注意,needClientAuth=true 是强制客户端证书验证的原因;没有这个,客户端可以在不提供可信证书的情况下进行连接。
要将 ApacheMQ 配置为使用上面定义的密钥和信任存储,请通过(在 Unix 上)定义环境变量 ACTIVEMQ_SSL_OPTS
export ACTIVEMQ_SSL_OPTS = -Djavax.net.ssl.keyStore=/path/to/broker.ks -Djavax.net.ssl.trustStore=/path/to/broker.ts -Djavax.net.ssl.keyStorePassword=passwordForBrokerKs -Djavax.net.ssl.trustStorePassword=passwordForBrokerTs
或(在 Windows 上)
set ACTIVEMQ_SSL_OPTS=-Djavax.net.ssl.keyStore=C:\path\to\broker.ks -Djavax.net.ssl.trustStore=C:\path\to\broker.ts -Djavax.net.ssl.keyStorePassword=passwordForBrokerKs -Djavax.net.ssl.trustStorePassword=passwordForBrokerTs
这里的两个密码是在上一步运行keytool后选择的。如果不需要客户端证书验证,只需省略trustStore 和trustStorePassword 的配置即可。
有了这个,ActiveMQ 可以像往常一样通过bin/activemq start 启动。为确保 SSL 配置符合预期,请注意启动服务器时输出的 JVM args 部分。
测试 STOMP 客户端
通过正确设置代理,我们也可以配置客户端。在这里,我们为stomp.Connection.set_ssl 提供了对第一步中创建的密钥和证书的引用。假设 ActiveMQ 服务器在 localhost:61613 上运行,你的测试脚本就变成了
import time
import sys
import stomp
class MyListener(stomp.ConnectionListener):
def on_error(self, headers, message):
print('received an error "%s"' % message)
def on_message(self, headers, message):
print('received a message "%s"' % message)
host = 'localhost'
port = 61613
conn = stomp.Connection([(host, port)])
conn.set_ssl(for_hosts=[(host, port)], key_file='/path/to/client.key', cert_file='/path/to/client.pem')
conn.set_listener('', MyListener())
conn.start()
conn.connect('admin', 'password', wait=True)
conn.subscribe(destination='/queue/test', id=1, ack='auto')
conn.send(body='test message', destination='/queue/test')
time.sleep(2)
conn.disconnect()
为确保 ApacheMQ 确实在验证客户端证书,我们可以重复步骤 1 并创建一个新对,例如 client2.key/client2.pem,然后使用它。这样做应该会导致 ApacheMQ 打印以下无意义的错误消息:
ERROR | Could not accept connection from null : {}
java.io.IOException: javax.net.ssl.SSLHandshakeException: General SSLEngine problem
验证代理证书
现在,细心的读者会注意到,我们实际上从未将代理证书移动到客户端,但无论如何,一切似乎都可以正常工作。事实证明,stomp.py 的默认行为是根本不执行证书验证,从而允许(主动)攻击者 MITM 连接。
由于我们正在滚动自签名证书,因此我们需要做的就是向 Python 客户端提供实际的代理证书。在broker上,通过
导出证书
keytool -exportcert -rfc -alias broker -keystore broker.ks -file broker.pem
并将 broker.pem 移动到 Python 客户端。现在,在上面的测试脚本中,通过将 SSL 配置替换为
来包含证书
conn.set_ssl(for_hosts=[(host, port)], key_file='/path/to/client.key', cert_file='/path/to/client.pem', ca_certs='/path/to/broker.pem')
如上所述,我们可以通过重复代理证书生成过程来创建一个broker2.pem,在测试脚本中使用它,并注意它会失败并显示一个
ssl.SSLError: [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed (_ssl.c:833)