| <?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. |
| |
| --> |
| |
| <chapter xmlns="http://docbook.org/ns/docbook" version="5.0" xml:id="JMS-Client-Message-Encryption"> |
| <title>Message Encryption</title> |
| |
| <para> |
| In some cases it is desirable to ensure no-one but the intended recipient(s) of a message will be able to read |
| its contents. Using SSL/TLS to encrypt traffic travelling between client and broker only ensures that those |
| snooping the network cannot read messages, however once the message arrives at the broker it is decrypted and |
| so anyone with access to the broker can read the message. For such confidential information it is necessary to |
| implement a mechanism of end-to-end encryption such that the sender of the message encrypts the message before |
| sending, and the recipient(s), upon receiving the message, decrypt it with some secret known only to them. |
| </para> |
| <para> |
| Neither JMS nor AMQP provide any defined mechanism for message encryption, however it is possible for any |
| application to build a message encryption scheme on top of a JMS API. For convenience the Client |
| provides a built in mechanism for encryption and decrypting messages. This mechanism is currently only |
| implemented in the Client for AMQP 0-8/0-9/0-9-1/0-10. If you use a different client you will be |
| unable to read encrypted messages. |
| </para> |
| |
| <section xml:id="JMS-Client-Message-Encryption-Overview"> |
| <title>Overview</title> |
| <para> |
| For each encrypted message which the client sends, a new message-specific secret key is generated. This |
| secret key is used encrypt the message contents using symmetric encryption (currently only AES-256 is |
| supported, although other algorithms may be added at a later date). For each intended recipient of the |
| message, the client encrypts the secret key using the public key associated with the recipient, and adds |
| this as a message header. On receipt of an encrypted message, the client looks to see if it has a private |
| key which can decrypt the secret key. If the client is unable to decrypt the message (for instance, because |
| they were not one of the intended recipients) then the message will be presented to the application as a |
| BytesMessage containing the encrypted data. |
| </para> |
| <para> |
| In order to send an encrypted message it is necessary to know the Certificates of the intended recipients. |
| Certificates can be distributed either through out-of-band mechanisms, or the Apache Qpid Broker-J can be used |
| to distribute them to clients. |
| </para> |
| <para> |
| In order to receive an encrypted message it is necessary to have a Certificate (which needs to be |
| distributed to those who you wish to send messages to you) and to have the private key associated with the |
| certificate so that you can decrypt messages sent to you. |
| </para> |
| <para> |
| This feature requires the Java Cryptography Extension (JCE) Unlimited Strength policy files are installed |
| in the JVM. |
| </para> |
| </section> |
| |
| <section xml:id="JMS-Client-Message-Encryption-Sending"> |
| <title>Sending an Encrypted Message</title> |
| <section xml:id="JMS-Client-Message-Encryption-Sending-Setting-TrustStore"> |
| <title>Providing the Trust Store</title> |
| <para> |
| In order for a connection to be capable of sending encrypted messages, it must be provided with a trust |
| store which contains the X509 certificates of the entities to which you wish to send. The details of the |
| trust store are supplied in the <link linkend="JMS-Client-0-8-Connection-URL">connection URL</link>. |
| </para> |
| <para> |
| There are two distinct mechanisms for providing the encryption trust store. Firstly you can supply a |
| standard password-protected trust store file on the file system. The location and password for this must |
| be specified using the <link linkend="JMS-Client-0-8-Connection-URL-BrokerOptions-EncryptionTrustStore"> |
| encryption_trust_store</link> and |
| <link linkend="JMS-Client-0-8-Connection-URL-BrokerOptions-EncryptionTrustStorePassword">encryption_trust_store_password |
| </link> options respectively. Such a connection URL might look somthing like: |
| </para> |
| <programlisting>amqp://username:password@clientid/test?brokerlist='tcp://localhost:5672?encryption_trust_store='/home/qpid/certificates.jks'&encryption_trust_store_password='password''</programlisting> |
| <para> |
| Alternatively, where available, you can configure the broker to distribute certificates from a trust |
| store (this is currently only available in the Apache Qpid Broker-J). In order to use this method, the broker |
| details in the connection url must contain the correctly configured |
| <link linkend="JMS-Client-0-8-Connection-URL-BrokerOptions-EncryptionRemoteTrustStore">encryption_remote_trust_store</link> |
| option. Such a connection URL might look somthing like: |
| </para> |
| <programlisting>amqp://username:password@clientid/test?brokerlist='tcp://localhost:5672?encryption_remote_trust_store='$certificates%255c/certstore''</programlisting> |
| <para> |
| The <literal>$certificates/</literal> prefix is mandatory. |
| However, in order to prevent the client from interpreting this the wrong way several layers of escaping and encoding need to take place. |
| The slash character ('/') needs to be escaped by a backslash ('\') which needs to be doubly URL encoded resulting in <literal>$certificates%255c/</literal>. |
| </para> |
| <para> |
| Note that to use the broker-distributed certificates the broker must be configured to expose the trust store as a message source. |
| See the broker documentation on TrustStores for more details. |
| </para> |
| </section> |
| <section xml:id="JMS-Client-Message-Encryption-Sending-Enabling-Encryption"> |
| <title>Enabling Encryption</title> |
| <para> |
| Message encryption can be enabled individually on each sent message, or - using configuration - all |
| messages sent to a Destination can be encrypted. |
| </para> |
| <para> |
| In order to encrypt messages on a case by case basis, the appliation must set the boolean property |
| <literal>x-qpid-encrypt</literal> to true on the message before sending. The intended recipients of the |
| message must also be set (see |
| <link linkend="JMS-Client-Message-Encryption-Sending-Choosing-Recipients">Choosing Recipients</link>). |
| </para> |
| <programlisting>message.setBooleanProperty("x-qpid-encrypt", true);</programlisting> |
| <para> |
| In order to encrypt all messages sent to a given Destination, the option |
| <link linkend="JMS-Client-0-8-Binding-URL-Options-SendEncrypted">sendencrypted</link> can be used. Note |
| that enabling encryption on the address can be overridden by explicitly setting the property |
| <literal>x-qpid-encrypt</literal> to false on an individual message. An example address would look like: |
| </para> |
| <programlisting>direct:///queue/queue?sendencrypted='true'</programlisting> |
| </section> |
| <section xml:id="JMS-Client-Message-Encryption-Sending-Choosing-Recipients"> |
| <title>Choosing Recipients</title> |
| <para> |
| Any message which is to be sent encrypted must also have a list of recipients who the sender wishes to |
| be able to decrypt the message. The recipients must be encoded as a semi-colon separated list of the |
| names given in the respective certificates of the recipients, e.g. |
| <literal>cn=first@example.org,ou=example,o=example,l=ny,st=ny,c=us;cn=second@example.org,ou=example,o=example,l=ny,st=ny,c=us</literal>. |
| </para> |
| <para> |
| As with enabling encryption, the recipients can be set either on a per-message basis or for all messages |
| sent to a given address. If both forms are used, the former overrides the latter. To set on an individual |
| message, set the String property <literal>x-qpid-encrypt-recipients</literal>. |
| </para> |
| <programlisting>message.setStringProperty("x-qpid-encrypt-recipients", "cn=only@example.org,ou=example,o=example");</programlisting> |
| <para> |
| To set the recipients on an address, use the address option |
| <link linkend="JMS-Client-0-8-Binding-URL-Options-EncryptedRecipients">encryptedrecipients</link>. |
| </para> |
| <programlisting>direct:///queue/queue?sendencrypted='true'&encryptedrecipients='cn=another@example.org,ou=example,o=example'</programlisting> |
| |
| </section> |
| <section xml:id="JMS-Client-Message-Encryption-Sending-Exposing-Properties"> |
| <title>Exposing Properties</title> |
| <para> |
| Message Encryption encrypts the message content and the properties set by the application. Sometimes |
| it is important to expose properties to allow (for example) message routing or message selectors within |
| the broker to work. To enable this it is possible to specify for each message all the properties which |
| the application wishes to make available to the broker. Note that exposing properties in this way means |
| that they are now visibe to anyone who can inspect the broker memory or file system stores. |
| </para> |
| <para> |
| To make message properties visible to the broker, set the String property |
| <literal>x-qpid-unencrypted-properties</literal> with a semi-colon separated list of the names of the |
| properties to be exposed. |
| </para> |
| <programlisting>message.setStringProperty("x-qpid-unencrypted-properties", "foo;bar;baz");</programlisting> |
| </section> |
| </section> |
| <section xml:id="JMS-Client-Message-Encryption-Receiving"> |
| <title>Receiving an Encrypted Message</title> |
| <section xml:id="JMS-Client-Message-Encryption-Sending-Setting-KeyStore"> |
| <title>Providing the Key Store</title> |
| <para> |
| In order for a connection to be capable of decrypting received encrypted messages, it must be provided |
| with a key store which contains the X509 certificates and associated Private Keys of the identities |
| on behalf of which it will be able to decrypt. The details of the |
| key store are supplied in the <link linkend="JMS-Client-0-8-Connection-URL">connection URL</link>. |
| The location and password for this must |
| be specified using the <link linkend="JMS-Client-0-8-Connection-URL-BrokerOptions-EncryptionKeyStore"> |
| encryption_key_store</link> and |
| <link linkend="JMS-Client-0-8-Connection-URL-BrokerOptions-EncryptionKeyStorePassword">encryption_trust_store_password |
| </link> options respectively. Such a connection URL might look somthing like: |
| </para> |
| <programlisting>amqp://username:password@clientid/test?brokerlist='tcp://localhost:5672?encryption_key_store='/home/qpid/identities.jks'&encryption_key_store_password='password''</programlisting> |
| |
| |
| </section> |
| </section> |
| <section xml:id="JMS-Client-Message-Encryption-Example"> |
| <title>Message Encryption Example</title> |
| <section xml:id="JMS-Client-Message-Encryption-Example-Introduction"> |
| <title>Introduction</title> |
| <para> |
| In this example we will setup the Qpid Broker-J and two clients who will send each other encrypted messages. |
| The clients will use self signed certificates and the certificates will be distributed by the Broker. |
| </para> |
| </section> |
| <section xml:id="JMS-Client-Message-Encryption-Example-Prerequisites"> |
| <title>Prerequisites</title> |
| <para> |
| For this example it is assumed the Broker is already running and that Management is enabled on port |
| 8443. |
| </para> |
| <para> |
| The example requires two (one for each client) self-signed X.509 certificates and the corresponding |
| keys. We refer to these as |
| <literal>client_1/2.cert</literal> |
| and |
| <literal>client_1/2.key</literal> |
| throughout the text below. |
| </para> |
| <para> |
| The following |
| <link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="https://www.openssl.org">openssl</link> |
| commands can be used to generate self signed certicates suitable for this test. |
| <programlisting> |
| <![CDATA[openssl req -x509 -newkey rsa:4096 -keyout client_1.key -out client_1.cert -days 365 -nodes -subj "/C=US/O=Apache/OU=Qpid/CN=client1" |
| openssl req -x509 -newkey rsa:4096 -keyout client_2.key -out client_2.cert -days 365 -nodes -subj "/C=US/O=Apache/OU=Qpid/CN=client2"]]> |
| </programlisting> |
| </para> |
| </section> |
| <section xml:id="JMS-Client-Message-Encryption-Example-Broker-Config"> |
| <title>Broker Configuration</title> |
| <para> |
| In this example we want the broker to distribute the client certificates. |
| Essentially, we want the broker to act as a<link xmlns:xlink="http://www.w3.org/1999/xlink" |
| xlink:href="https://en.wikipedia.org/wiki/Key_server_(cryptographic)"> |
| Key Server</link>. |
| Use |
| <link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="${oracleKeytool}">java's keytool</link> |
| to create a trust store containing the two client certificates. |
| <programlisting> |
| <![CDATA[keytool -importcert -file client_1.cert -alias client1 -keystore mytruststore.jks |
| keytool -importcert -file client_2.cert -alias client2 -keystore mytruststore.jks]]> |
| </programlisting> |
| Now a FileTrustStore can be created on the broker pointing to the java trust store that was just |
| created. |
| This can be done via the Web Management Console or the REST API: |
| <programlisting>curl -v -u admin https://localhost:8443/api/v6.1/truststore/clientcerts -X PUT -d |
| '{"type": "FileTrustStore", "stroeUrl": "<path_to_truststore>", "password": "<your_truststore_password>"}' |
| </programlisting> |
| The TrustStore must be configured to expose the certificates as a message source to the clients: |
| <programlisting>curl -v -u admin https://localhost:8443/api/v6.1/truststore/clientcerts -X POST -d |
| '{"exposedAsMessageSource": true}' |
| </programlisting> |
| </para> |
| </section> |
| <section xml:id="JMS-Client-Message-Encryption-Example-Client-Config"> |
| <title>Client Configuration</title> |
| <para> |
| The configuration for the clients happens in the connection URL. In this example this is provided via a |
| JNDI properties file. |
| </para> |
| <para> |
| On the producing side, in order to encrypt a message for a recipient, the Qpid client needs the |
| recipient's public certificate which is distributed by the Broker following our above broker setup. The |
| <literal>encryption_remote_trust_store</literal> |
| element within the connection URL provides the name of the truststore. |
| </para> |
| <para> |
| On the receiving side, in order to decrypt a message it needs a JKS keystore with the private key |
| matching the public certificate. |
| For this example, the keystores can again be created with a two-step process involving |
| <link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="https://www.openssl.org">openssl</link> |
| and <link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="${oracleKeytool}">java's keytool</link>. |
| <programlisting> |
| <![CDATA[openssl pkcs12 -export -in client_1.cert -inkey client_1.key -out client_1.pkcs12 -name "client1" |
| openssl pkcs12 -export -in client_2.cert -inkey client_2.key -out client_2.pkcs12 -name "client2" |
| |
| keytool -importkeystore -destkeystore client_1.jks -srckeystore client_1.pkcs12 -srcstoretype PKCS12 |
| keytool -importkeystore -destkeystore client_2.jks -srckeystore client_2.pkcs12 -srcstoretype PKCS12]]> |
| </programlisting> |
| |
| </para> |
| <para> |
| The final JNDI properties file should look similar to this: |
| <programlisting> |
| java.naming.factory.initial = org.apache.qpid.jndi.PropertiesFileInitialContextFactory |
| |
| # connection factories. This is where end-to-end encryption is configured on the client. |
| # connectionfactory.[jndiname] = [ConnectionURL] |
| connectionfactory.producerConnectionFactory = amqp://<username>:<password>@/?brokerlist='tcp://localhost:5672?encryption_remote_trust_store='$certificates%255c/clientcerts'' |
| connectionfactory.consumer1ConnectionFactory = amqp://<username>:<password>@/?brokerlist='tcp://localhost:5672?encryption_key_store='path/to/client_1.jks'&encryption_key_store_password='<keystore_password>'' |
| connectionfactory.consumer2ConnectionFactory = amqp://<username>:<password>@/?brokerlist='tcp://localhost:5672?encryption_key_store='path/to/client_2.jks'&encryption_key_store_password='<keystore_password>'' |
| |
| # Rest of JNDI configuration. For example |
| # destination.[jniName] = [Address Format] |
| queue.myTestQueue = testQueue |
| </programlisting> |
| </para> |
| </section> |
| <section xml:id="JMS-Client-Message-Encryption-Example-Application"> |
| <title>Application Code</title> |
| <para> |
| On the producing side, the application needs to enable encryption and indicate the intended recipient(s) |
| of each message. This is done via the |
| <literal>x-qpid-encrypt</literal> |
| and |
| <literal>x-qpid-encrypt-recipients</literal> |
| message properties. Note that the order of the relative distinguished name (RDN) entries within the |
| recipent's distinguished name (DNs) is significant. If the order does not match that recorded in |
| truststore, a |
| <link linkend="JMS-Client-0-8-Appendix-Exceptions-CertificateException">CertificateException</link> |
| will be encountered. |
| </para> |
| <para> |
| On the receiving side, there is nothing to do. The application code does not have to add decryption code as this is handled transparently by the Qpid client library. |
| However, the receiving application should gracefully handle failures in decryption in which case the encrypted message will be delivered as a BytesMessage. |
| <programlisting language="java"> |
| // imports omitted for brevity |
| |
| public class EncryptionExample { |
| public EncryptionExample() { |
| } |
| |
| public static void main(String[] args) throws Exception { |
| EncryptionExample encryptionExampleApp = new EncryptionExample(); |
| encryptionExampleApp.runProducerExample(); |
| encryptionExampleApp.runReceiverExample(); |
| } |
| |
| private void runProducerExample() throws Exception |
| { |
| Connection connection = createConnection("producerConnectionFactory"); |
| try { |
| Session session = connection.createSession(true, Session.SESSION_TRANSACTED); |
| Destination destination = createDesination("myTestQueue"); |
| |
| MessageProducer messageProducer = session.createProducer(destination); |
| TextMessage message = session.createTextMessage("Hello world!"); |
| |
| // ============== Enable encryption for this message ============== |
| message.setBooleanProperty("x-qpid-encrypt", true); |
| // ============== Configure recipients for encryption ============== |
| message.setStringProperty("x-qpid-encrypt-recipients", "CN=client1, OU=Qpid, O=Apache, C=US"); |
| |
| messageProducer.send(message); |
| session.commit(); |
| } |
| finally { |
| connection.close(); |
| } |
| } |
| |
| private void runReceiverExample() throws Exception |
| { |
| Connection connection = createConnection("consumer1ConnectionFactory"); |
| try { |
| connection.start(); |
| Session session = connection.createSession(true, Session.SESSION_TRANSACTED); |
| Destination destination = createDesination("myTestQueue"); |
| MessageConsumer messageConsumer = session.createConsumer(destination); |
| Message message = messageConsumer.receive(); |
| if (message instanceof TextMessage) { |
| // application logic |
| System.out.println(((TextMessage) message).getText()); |
| } else if (message instanceof BytesMessage) { |
| // handle potential decryption failure |
| System.out.println("Potential decryption problem. Application not in list of intended recipients?"); |
| } |
| session.commit(); |
| } |
| finally { |
| connection.close(); |
| } |
| } |
| |
| /////////////////////////////////////// |
| // The following is boilerplate code // |
| /////////////////////////////////////// |
| |
| private Connection createConnection(final String connectionFactoryName) throws JMSException, IOException, NamingException |
| { |
| try (InputStream resourceAsStream = this.getClass().getResourceAsStream("example.properties")) { |
| Properties properties = new Properties(); |
| properties.load(resourceAsStream); |
| Context context = new InitialContext(properties); |
| ConnectionFactory connectionFactory = (ConnectionFactory) context.lookup(connectionFactoryName); |
| final Connection connection = connectionFactory.createConnection(); |
| context.close(); |
| return connection; |
| } |
| } |
| |
| private Destination createDesination(String desinationJndiName) throws IOException, NamingException |
| { |
| try (InputStream resourceAsStream = this.getClass().getResourceAsStream("example.properties")) { |
| Properties properties = new Properties(); |
| properties.load(resourceAsStream); |
| Context context = new InitialContext(properties); |
| Destination destination = (Destination) context.lookup(desinationJndiName); |
| context.close(); |
| return destination; |
| } |
| } |
| } |
| </programlisting> |
| </para> |
| </section> |
| </section> |
| </chapter> |