TLS and HTTPS configuration in default developer config

This commit is contained in:
Arthur Barr
2018-03-20 16:47:24 +00:00
parent 913465620b
commit ab3b44e84b
21 changed files with 849 additions and 42 deletions

View File

@@ -18,14 +18,15 @@ limitations under the License.
package main
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"path/filepath"
"testing"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
"github.com/docker/go-connections/nat"
)
func TestDevGoldenPath(t *testing.T) {
@@ -35,7 +36,12 @@ func TestDevGoldenPath(t *testing.T) {
t.Fatal(err)
}
containerConfig := container.Config{
Env: []string{"LICENSE=accept", "MQ_QMGR_NAME=qm1"},
Env: []string{
"LICENSE=accept",
"MQ_QMGR_NAME=qm1",
// TODO: Use default password (not set) here
"MQ_APP_PASSWORD=" + devAppPassword,
},
}
id := runContainer(t, cli, &containerConfig)
@@ -43,30 +49,74 @@ func TestDevGoldenPath(t *testing.T) {
waitForReady(t, cli, id)
waitForWebReady(t, cli, id)
timeout := time.Duration(30 * time.Second)
// Disable TLS verification (server uses a self-signed certificate by default,
// so verification isn't useful anyway)
tr := &http.Transport{
TLSClientConfig: &tls.Config{
t.Run("REST", func(t *testing.T) {
// Disable TLS verification (server uses a self-signed certificate by default,
// so verification isn't useful anyway)
testREST(t, cli, id, &tls.Config{
InsecureSkipVerify: true,
},
}
httpClient := http.Client{
Timeout: timeout,
Transport: tr,
}
url := fmt.Sprintf("https://localhost:%s/ibmmq/rest/v1/admin/installation", getWebPort(t, cli, id))
req, err := http.NewRequest("GET", url, nil)
req.SetBasicAuth("admin", "passw0rd")
resp, err := httpClient.Do(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected HTTP status code %v from 'GET installation'; got %v", http.StatusOK, resp.StatusCode)
}
})
})
t.Run("JMS", func(t *testing.T) {
runJMSTests(t, cli, id, false)
})
// Stop the container cleanly
stopContainer(t, cli, id)
}
func TestDevTLS(t *testing.T) {
t.Parallel()
cli, err := client.NewEnvClient()
if err != nil {
t.Fatal(err)
}
const tlsPassPhrase string = "passw0rd"
containerConfig := container.Config{
Env: []string{
"LICENSE=accept",
"MQ_QMGR_NAME=qm1",
"MQ_APP_PASSWORD=" + devAppPassword,
"MQ_TLS_KEYSTORE=/var/tls/server.p12",
"MQ_TLS_PASSPHRASE=" + tlsPassPhrase,
"DEBUG=1",
},
Image: imageName(),
}
hostConfig := container.HostConfig{
Binds: []string{
coverageBind(t),
tlsDir(t) + ":/var/tls",
},
// Assign a random port for the web server on the host
// TODO: Don't do this for all tests
PortBindings: nat.PortMap{
"9443/tcp": []nat.PortBinding{
{
HostIP: "0.0.0.0",
},
},
},
}
networkingConfig := network.NetworkingConfig{}
ctr, err := cli.ContainerCreate(context.Background(), &containerConfig, &hostConfig, &networkingConfig, t.Name())
if err != nil {
t.Fatal(err)
}
defer cleanContainer(t, cli, ctr.ID)
startContainer(t, cli, ctr.ID)
waitForReady(t, cli, ctr.ID)
waitForWebReady(t, cli, ctr.ID)
t.Run("REST", func(t *testing.T) {
// Use the correct certificate for the HTTPS connection
cert := filepath.Join(tlsDir(t), "server.crt")
testREST(t, cli, ctr.ID, createTLSConfig(t, cert, tlsPassPhrase))
})
t.Run("JMS", func(t *testing.T) {
runJMSTests(t, cli, ctr.ID, true)
})
// Stop the container cleanly
stopContainer(t, cli, ctr.ID)
}

View File

@@ -18,14 +18,26 @@ limitations under the License.
package main
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"testing"
"time"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/network"
"github.com/docker/docker/client"
)
const devAdminPassword string = "passw0rd"
const devAppPassword string = "passw0rd"
func waitForWebReady(t *testing.T, cli *client.Client, ID string) {
config := tls.Config{InsecureSkipVerify: true}
a := fmt.Sprintf("localhost:%s", getWebPort(t, cli, ID))
@@ -34,9 +46,101 @@ func waitForWebReady(t *testing.T, cli *client.Client, ID string) {
if err == nil {
conn.Close()
// Extra sleep to allow web apps to start
time.Sleep(3 * time.Second)
time.Sleep(5 * time.Second)
t.Log("MQ web server is ready")
return
}
time.Sleep(1 * time.Second)
}
}
// tlsDir returns the host directory where the test certificate(s) are located
func tlsDir(t *testing.T) string {
dir, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
return filepath.Join(dir, "../tls")
}
// runJMSTests runs a container with a JMS client, which connects to the queue manager container with the specified ID
func runJMSTests(t *testing.T, cli *client.Client, ID string, tls bool) {
containerConfig := container.Config{
// -e MQ_PORT_1414_TCP_ADDR=9.145.14.173 -e MQ_USERNAME=app -e MQ_PASSWORD=passw0rd -e MQ_CHANNEL=DEV.APP.SVRCONN -e MQ_TLS_KEYSTORE=/tls/test.p12 -e MQ_TLS_PASSPHRASE=passw0rd -v /Users/arthurbarr/go/src/github.com/ibm-messaging/mq-container/test/tls:/tls msgtest
Env: []string{
"MQ_PORT_1414_TCP_ADDR=" + getIPAddress(t, cli, ID),
"MQ_USERNAME=app",
"MQ_PASSWORD=" + devAppPassword,
"MQ_CHANNEL=DEV.APP.SVRCONN",
},
Image: "msgtest",
}
if tls {
t.Log("Using TLS from JMS client")
containerConfig.Env = append(containerConfig.Env, []string{
"MQ_TLS_TRUSTSTORE=/var/tls/client-trust.jks",
"MQ_TLS_PASSPHRASE=passw0rd",
}...)
}
hostConfig := container.HostConfig{
Binds: []string{
coverageBind(t),
tlsDir(t) + ":/var/tls",
},
}
networkingConfig := network.NetworkingConfig{}
ctr, err := cli.ContainerCreate(context.Background(), &containerConfig, &hostConfig, &networkingConfig, strings.Replace(t.Name(), "/", "", -1))
if err != nil {
t.Fatal(err)
}
startContainer(t, cli, ctr.ID)
rc := waitForContainer(t, cli, ctr.ID, 10)
if rc != 0 {
t.Errorf("JUnit container failed with rc=%v", rc)
}
defer cleanContainer(t, cli, ctr.ID)
}
// createTLSConfig creates a tls.Config which trusts the specified certificate
func createTLSConfig(t *testing.T, certFile, password string) *tls.Config {
// Get the SystemCertPool, continue with an empty pool on error
certs, err := x509.SystemCertPool()
if err != nil {
t.Fatal(err)
}
// Read in the cert file
cert, err := ioutil.ReadFile(certFile)
if err != nil {
t.Fatal(err)
}
// Append our cert to the system pool
ok := certs.AppendCertsFromPEM(cert)
if !ok {
t.Fatal("No certs appended")
}
// Trust the augmented cert pool in our client
return &tls.Config{
InsecureSkipVerify: false,
RootCAs: certs,
}
}
func testREST(t *testing.T, cli *client.Client, ID string, tlsConfig *tls.Config) {
httpClient := http.Client{
Timeout: time.Duration(30 * time.Second),
Transport: &http.Transport{
TLSClientConfig: tlsConfig,
},
}
url := fmt.Sprintf("https://localhost:%s/ibmmq/rest/v1/admin/installation", getWebPort(t, cli, ID))
req, err := http.NewRequest("GET", url, nil)
req.SetBasicAuth("admin", devAdminPassword)
resp, err := httpClient.Do(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != http.StatusOK {
t.Errorf("Expected HTTP status code %v from 'GET installation'; got %v", http.StatusOK, resp.StatusCode)
}
}

6
test/messaging/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.classpath
.gradle
.project
.settings
bin

38
test/messaging/Dockerfile Normal file
View File

@@ -0,0 +1,38 @@
# © Copyright IBM Corporation 2018
#
# 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
#
# 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.
###############################################################################
# Application build environment (Gradle)
###############################################################################
FROM gradle as builder
ENV GRADLE_OPTS="-Dorg.gradle.daemon=false"
# Change where Gradle stores its cache, so that it's not under a volume
# (and therefore gets cached by Docker)
ENV GRADLE_USER_HOME=/home/gradle/gradle
RUN mkdir -p $GRADLE_USER_HOME
COPY --chown=gradle build.gradle /app/
WORKDIR /app
# Download dependencies separately, so Docker caches them
RUN gradle download
# Copy source
COPY --chown=gradle src /app/src
# Run the main build
RUN gradle install
###############################################################################
# Application runtime (JRE only, no build environment)
###############################################################################
FROM ibmjava:sfj
COPY --from=builder /app/lib/*.jar /opt/app/
ENTRYPOINT ["java", "-classpath", "/opt/app/*", "org.junit.platform.console.ConsoleLauncher", "-p", "com.ibm.mqcontainer.test", "--details", "verbose"]

View File

@@ -0,0 +1,39 @@
# © Copyright IBM Corporation 2018
#
# 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
#
# 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.
apply plugin: 'java'
repositories {
mavenCentral()
}
dependencies {
compile group: 'com.ibm.mq', name: 'com.ibm.mq.allclient', version: '9.0.4.0'
compile "org.junit.jupiter:junit-jupiter-api:5.0.3"
runtime "org.junit.jupiter:junit-jupiter-engine:5.0.3"
runtime "org.junit.platform:junit-platform-console-standalone:1.0.3"
}
task download(type: Exec) {
configurations.runtime.files
commandLine 'echo', 'Downloaded all dependencies'
}
// Copy all dependencies to the lib directory
task install(type: Copy) {
dependsOn build
from configurations.runtime
from jar
into "${project.projectDir}/lib"
}

View File

@@ -0,0 +1,144 @@
/*
© Copyright IBM Corporation 2018
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
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 com.ibm.mqcontainer.test;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import java.io.FileInputStream;
import java.io.IOException;
import java.net.Socket;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.util.logging.Logger;
import javax.jms.JMSContext;
import javax.jms.JMSException;
import javax.jms.Message;
import javax.jms.Queue;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
import com.ibm.mq.jms.MQConnectionFactory;
import com.ibm.mq.jms.MQQueue;
import com.ibm.msg.client.wmq.WMQConstants;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;
class JMSTests {
private static final Logger LOGGER = Logger.getLogger(JMSTests.class.getName());
protected static final String ADDR = System.getenv("MQ_PORT_1414_TCP_ADDR");
protected static final String USER = System.getenv("MQ_USERNAME");
protected static final String PASSWORD = System.getenv("MQ_PASSWORD");
protected static final String CHANNEL = System.getenv("MQ_CHANNEL");
protected static final String TRUSTSTORE = System.getenv("MQ_TLS_TRUSTSTORE");
protected static final String PASSPHRASE = System.getenv("MQ_TLS_PASSPHRASE");
private JMSContext context;
static SSLSocketFactory createSSLSocketFactory() throws IOException, GeneralSecurityException {
KeyStore ts=KeyStore.getInstance("jks");
ts.load(new FileInputStream(TRUSTSTORE), PASSPHRASE.toCharArray());
// KeyManagerFactory kmf=KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
TrustManagerFactory tmf=TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(ts);
// tmf.init();
SSLContext ctx = SSLContext.getInstance("TLSv1.2");
// Security.setProperty("crypto.policy", "unlimited");
ctx.init(null, tmf.getTrustManagers(), null);
return ctx.getSocketFactory();
}
static JMSContext create(String channel, String addr, String user, String password) throws JMSException, IOException, GeneralSecurityException {
LOGGER.info(String.format("Connecting to %s/TCP/%s(1414) as %s", channel, addr, user));
MQConnectionFactory factory = new MQConnectionFactory();
factory.setTransportType(WMQConstants.WMQ_CM_CLIENT);
factory.setChannel(channel);
factory.setConnectionNameList(String.format("%s(1414)", addr));
// factory.setClientReconnectOptions(WMQConstants.WMQ_CLIENT_RECONNECT);
if (TRUSTSTORE == null) {
LOGGER.info("Not using TLS");
}
else {
LOGGER.info(String.format("Using TLS. Trust store=%s", TRUSTSTORE));
SSLSocketFactory ssl = createSSLSocketFactory();
factory.setSSLSocketFactory(ssl);
factory.setSSLCipherSuite("SSL_RSA_WITH_AES_128_CBC_SHA256");
// LOGGER.info(Arrays.toString(ssl.getSupportedCipherSuites()));
}
// Give up if unable to reconnect for 10 minutes
// factory.setClientReconnectTimeout(600);
// LOGGER.info(String.format("user=%s pw=%s", user, password));
return factory.createContext(user, password);
}
@BeforeAll
private static void waitForQueueManager() {
for (int i = 0; i < 20; i++) {
try {
Socket s = new Socket(ADDR, 1414);
s.close();
return;
} catch (IOException e) {
try {
Thread.sleep(500);
} catch (InterruptedException ex) {
}
}
}
}
@BeforeEach
void connect() throws Exception {
context = create(CHANNEL, ADDR, USER, PASSWORD);
}
@Test
void succeedingTest(TestInfo t) throws JMSException {
Queue queue = new MQQueue("DEV.QUEUE.1");
context.createProducer().send(queue, t.getDisplayName());
Message m = context.createConsumer(queue).receive();
assertNotNull(m.getBody(String.class));
}
// @Test
// void failingTest() {
// fail("a failing test");
// }
@Test
@Disabled("for demonstration purposes")
void skippedTest() {
// not executed
}
@AfterEach
void tearDown() {
if (context != null) {
context.close();
}
}
@AfterAll
static void tearDownAll() {
}
}

BIN
test/tls/client-trust.jks Normal file

Binary file not shown.

41
test/tls/generate-test-cert.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/bin/bash
# -*- mode: sh -*-
# © Copyright IBM Corporation 2018
#
# 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
#
# 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.
KEY=server.key
CERT=server.crt
PKCS=server.p12
PASSWORD=passw0rd
# Create a private key and certificate in PEM format, for the server to use
openssl req \
-newkey rsa:2048 -nodes -keyout ${KEY} \
-subj "/CN=localhost" \
-x509 -days 365 -out ${CERT}
# Add the key and certificate to a PKCS #12 key store, for the server to use
openssl pkcs12 \
-inkey ${KEY} \
-in ${CERT} \
-export -out ${PKCS} \
-password pass:${PASSWORD}
# Add the certificate to a trust store in JKS format, for Java clients to use when connecting
keytool -import \
-alias server-cert \
-file ${CERT} \
-keystore client-trust.jks \
-storepass ${PASSWORD} \
-noprompt

17
test/tls/server.crt Normal file
View File

@@ -0,0 +1,17 @@
-----BEGIN CERTIFICATE-----
MIICpDCCAYwCCQDft9xlN4fNFTANBgkqhkiG9w0BAQsFADAUMRIwEAYDVQQDDAls
b2NhbGhvc3QwHhcNMTgwMzIwMTUxODMwWhcNMTkwMzIwMTUxODMwWjAUMRIwEAYD
VQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDk
XzX0xQIZzKVX8/lDQh5lSHr5U9cBL+kURA3fEgl3ks9KjZPggfxWl4Y5dekChW/s
iknVssoNw9vI1W25qtQ81zRFQbHbpej0lLdYsS8/yZCuAVjMTp6Q9IswTwhVA6OD
5orag5dH3XQH+GsnmGXRCY7Gs93onAe3i3ShX9qpUFOJXyxCX+pLAC6kWQ3f/HI8
dujVXKsg1vHgOgGqQGwnh8gm5OeWUeuTMdD2v7Hn1OxilgNMbcewA7bpvipgm2xt
ZD0PKFDmtQ4comr25Oo+eUf1N7jSpRPOWJNxoyS9/coQUPp1Gpbk7khYHjGn7f5a
EZqQ4Hmwwh50uT+vKVxDAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAAHaywC7ZLOi
3PKlidj6PWe33dEVsDL6RRb3cOqR86Ld2aD91oLrpELRhz4v2mt/GfQMIg7rc6z7
26SuPzV/7zZAv1N/vGoIFyvBXWLYP5qCwUrmykcH/wfFM80S6FJxz5Wy5MA5UzTB
HdpiQCPu4U0IKgATLDraz0xlQ61Rog56YhgJI8ulHuav5iYxqV2mwU09Hs0kXPJ7
g0PLRaSyidsXafxBKukeM9QHl8z8HN8er23oqecYo59b/Bt0c6jSrJCK39EUcoLP
HxR+Ma1SPhVKGqa3lPmaoAzsFTqaJ6fsIcbp+oEFAq0LPeqMPK7u3ygT4iTblAl8
q3isCz4Ytx4=
-----END CERTIFICATE-----

28
test/tls/server.key Normal file
View File

@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDkXzX0xQIZzKVX
8/lDQh5lSHr5U9cBL+kURA3fEgl3ks9KjZPggfxWl4Y5dekChW/siknVssoNw9vI
1W25qtQ81zRFQbHbpej0lLdYsS8/yZCuAVjMTp6Q9IswTwhVA6OD5orag5dH3XQH
+GsnmGXRCY7Gs93onAe3i3ShX9qpUFOJXyxCX+pLAC6kWQ3f/HI8dujVXKsg1vHg
OgGqQGwnh8gm5OeWUeuTMdD2v7Hn1OxilgNMbcewA7bpvipgm2xtZD0PKFDmtQ4c
omr25Oo+eUf1N7jSpRPOWJNxoyS9/coQUPp1Gpbk7khYHjGn7f5aEZqQ4Hmwwh50
uT+vKVxDAgMBAAECggEBAL91kybChCBdEcHLKQ7aP+FqAq9FOtwj7qSu6XI7DPTS
gDdgurleQM/X+Q/zaoZSmKMWzQ/79KnVqk2VoYgnUAgx5ACsMxCS59slUxFoetRf
iIxZVLj0sLuWSZsWp0We51eN0Juh9xKo9r435p4rhjDacnjkEwcQyOd4Yy9nzUpk
GDD5Vu1J9bOOKUQZ0qgjPyl/xWiwD1yfGJ0nHpQ5ucfrCO9p+n7SYsx01WcAkC8J
WP9XSXgi5uIefTWb/4m2b32jzjIgzAHkNx6yktRTjBJ7QILnKq1P8JjkNA/Awj4P
OxAz9hHHnVRuq4ZlEqfvo9p9YAbN2IH5TnmN3rGCXwECgYEA9JitVIeXCS0qIMFA
dKCmm9CT7JXccdpVllwaaYCNTb+G2RBrJqAvQEetoYJodWTIm1mNwSEORFFw0W+N
eaMzibJoJ+MZHRhiulDJaY0vwAKHkSJjDPJrPLgGMCUOLiWSAAnR4z35WfeY0e//
JbdZZemrJRyzy3o6rkRN9TQcUMUCgYEA7wTj5w5GZ8NQ7Nn8nIS2ayk+woIMHS+g
RVFufJoBeopsNJfNzGak0s+nz5q0nMGMzQsxXkbmAOLMTU3woQ7cEGjkLAfoch23
ACOe7M4rZbIk6kVNOlFESWdVdWViVd/B2a7oBqOIykoqX6VSqqrw+xghAUmd/2W1
uxjg9v01OWcCgYApE5LYRUUKF3mhspKeg3Q3apnM+4Xf4OjKrYEKArq4OdftkCJO
hEwrIV55Zysfu+Mso6d4rZJ1yq+FnJRHvy6ii0GOoUbQag36eCK7BSjluAcISpwT
yopT0hvH7hEpksmoE/4ZiYjcoQYbC5DvxpDO2qURQHa5TzeXmIT3Dt9KeQKBgQC6
UKeOXrRHAhs85ZdiMpk340jGujTTM2LNZfKoMixg5zH9tS9427IzmicHT2LmpoEo
/EaZZM65dhEnWU/vW/Py3rCuGeP5wGv8Mcgac4OknD7mVusiQGLojSIyhrsmkWs8
UnkPY76nYTSypd5Qpzt9n4tqw4XjpdcJZxVFso8glQKBgQCHlb15As73En/Q2AxL
5FY1Q1lLuO8y33ZZIRK4eynOKkbiuAh7X+ONZ4T9NtTm2J7mnltvTHZ7yeOI+VLS
LrTTBwnnNfdpp8UVPQlwzeizoDqSbr1sjFYvKOfdDDfxuzieT/4tfW9VTAxn4uOg
qpg7aRMUYUuLAH+S5atdOqXB+g==
-----END PRIVATE KEY-----

BIN
test/tls/server.p12 Normal file

Binary file not shown.