Menu Search

9.4. Message Encryption Example

9.4.1. Introduction

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.

9.4.2. Prerequisites

For this example it is assumed the Broker is already running and that Management is enabled on port 8443.

The example requires two (one for each client) self-signed X.509 certificates and the corresponding keys. We refer to these as client_1/2.cert and client_1/2.key throughout the text below.

The following openssl commands can be used to generate self signed certicates suitable for this test.

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"
                

9.4.3. Broker Configuration

In this example we want the broker to distribute the client certificates. Essentially, we want the broker to act as a Key Server. Use java's keytool to create a trust store containing the two client certificates.

keytool -importcert -file client_1.cert -alias client1 -keystore mytruststore.jks
keytool -importcert -file client_2.cert -alias client2 -keystore mytruststore.jks
                

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:

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>"}'
                

The TrustStore must be configured to expose the certificates as a message source to the clients:

curl -v -u admin https://localhost:8443/api/v6.1/truststore/clientcerts -X POST -d
                    '{"exposedAsMessageSource": true}'
                

9.4.4. Client Configuration

The configuration for the clients happens in the connection URL. In this example this is provided via a JNDI properties file.

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 encryption_remote_trust_store element within the connection URL provides the name of the truststore.

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 openssl and java's keytool.

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
                

The final JNDI properties file should look similar to this:

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
                

9.4.5. Application Code

On the producing side, the application needs to enable encryption and indicate the intended recipient(s) of each message. This is done via the x-qpid-encrypt and x-qpid-encrypt-recipients 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 CertificateException will be encountered.

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.

// 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;
        }
    }
}