From 369965abc6e77263668e348428fc0842db5428f3 Mon Sep 17 00:00:00 2001 From: Ahmad Fatoum Date: Thu, 24 Oct 2024 20:41:50 +0200 Subject: [PATCH] PROTON-2594: [C++] add test for newly added PKCS#11 support Existing tests hardcode paths to PEM files. For easily testing PKCS#11 usage for client certificates on the target, we want to pass in dynamically PKCS#11 URIs identifying the certificates and keys to use without requiring recompilation. Enable doing that by consulting a set of new environment variables: PKCS11_CLIENT_CERT: URI of client certificate PKCS11_CLIENT_KEY: URI of client private key PKCS11_SERVER_CERT: URI of server certificate PKCS11_SERVER_KEY: URI of server private key PKCS11_CA_CERT: URI of CA certificate These variables are populated and exported by sourcing the new scripts/prep-pkcs11_test.sh script prior to executing the test. The script uses SoftHSM, which is an implementation of a cryptographic store accessible through a PKCS #11 interface without requiring an actual Hardware Security Module (HSM). We load into the SoftHSM both client and server keys and certificates. As the server key exists only in encrypted form, we decrypt server-private-key-lh.pem, so we need not handle passphrase input when the PEM file is processed by pkcs11-tool. When the script is not sourced, none of the environment variables will be set and the test will be skipped without being marked as error. --- .github/workflows/build.yml | 16 ++- ci/pkcs11-provider.sh | 38 ++++++ cpp/src/pkcs11_test.cpp | 113 ++++++++++++++++++ cpp/testdata/certs/make_certs.sh | 1 + .../server-private-key-lh-no-password.pem | 28 +++++ cpp/tests.cmake | 10 ++ scripts/openssl-pkcs11.cnf | 22 ++++ scripts/prep-pkcs11_test.sh | 85 +++++++++++++ scripts/softhsm2.conf.in | 16 +++ 9 files changed, 326 insertions(+), 3 deletions(-) create mode 100755 ci/pkcs11-provider.sh create mode 100644 cpp/src/pkcs11_test.cpp create mode 100644 cpp/testdata/certs/server-private-key-lh-no-password.pem create mode 100644 scripts/openssl-pkcs11.cnf create mode 100644 scripts/prep-pkcs11_test.sh create mode 100644 scripts/softhsm2.conf.in diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8e7ba3b3e..6c2477b1b0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ jobs: fail-fast: false matrix: os: - - ubuntu-latest + - ubuntu-24.04 - macOS-13 - windows-latest buildType: @@ -47,7 +47,7 @@ jobs: - name: Install Linux dependencies if: runner.os == 'Linux' run: | - sudo apt install -y swig libpython3-dev libsasl2-dev libjsoncpp-dev + sudo apt install -y swig libpython3-dev libsasl2-dev libjsoncpp-dev softhsm2 opensc - name: Install Windows dependencies if: runner.os == 'Windows' run: | @@ -63,6 +63,10 @@ jobs: working-directory: ${{github.workspace}} run: sudo sh ./ci/otel.sh shell: bash + - name: pkcs11-provider build/install + if: runner.os == 'Linux' + run: sudo sh ./ci/pkcs11-provider.sh + shell: bash - name: cmake configure working-directory: ${{env.BuildDir}} run: cmake "${{github.workspace}}" "-DCMAKE_BUILD_TYPE=${BuildType}" "-DCMAKE_INSTALL_PREFIX=${InstallPrefix}" ${{matrix.cmake_extra}} @@ -88,7 +92,13 @@ jobs: - id: ctest name: ctest working-directory: ${{env.BuildDir}} - run: ctest -C ${BuildType} -V -T Test --no-compress-output ${{matrix.ctest_extra}} + run: | + if [ "$RUNNER_OS" = "Linux" ]; then + pushd ${{github.workspace}} + . scripts/prep-pkcs11_test.sh + popd + fi + ctest -C ${BuildType} -V -T Test --no-compress-output ${{matrix.ctest_extra}} shell: bash - name: Upload Test results if: always() && (steps.ctest.outcome == 'failure' || steps.ctest.outcome == 'success') diff --git a/ci/pkcs11-provider.sh b/ci/pkcs11-provider.sh new file mode 100755 index 0000000000..0354e5862e --- /dev/null +++ b/ci/pkcs11-provider.sh @@ -0,0 +1,38 @@ +# +# 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. +# + +set -e + +# pkcs11-provider dependencies + +sudo apt-get install meson + +# Clone pkcs11-provider + +git clone -b v0.5 https://github.com/latchset/pkcs11-provider + +# Build/Install pkcs11-provider + +cd pkcs11-provider +mkdir build + +meson setup build . +meson compile -C build +meson install -C build +cd .. diff --git a/cpp/src/pkcs11_test.cpp b/cpp/src/pkcs11_test.cpp new file mode 100644 index 0000000000..4677de535a --- /dev/null +++ b/cpp/src/pkcs11_test.cpp @@ -0,0 +1,113 @@ +/* + * 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. + */ + +#include "test_bits.hpp" + +#include "proton/connection_options.hpp" +#include "proton/container.hpp" +#include "proton/ssl.hpp" + +// The C++ API lacks a way to test for presence of extended SSL support. +#include "proton/ssl.h" + +#include +#include + +#include "test_handler.hpp" + +#define SKIP_RETURN_CODE 127 + +namespace { + +using namespace std; +using namespace proton; + +// Hack to write strings with embedded '"' and newlines +#define RAW_STRING(...) #__VA_ARGS__ + +static const char *client_cert, *client_key, + *server_cert, *server_key, + *ca_cert; + +class test_tls_external : public test_handler { + + static connection_options make_opts() { + ssl_certificate cert(server_cert, server_key); + connection_options opts; + opts.ssl_server_options(ssl_server_options(cert, ca_cert, + ca_cert, ssl::VERIFY_PEER)); + return opts; + } + + public: + + test_tls_external() : test_handler(make_opts()) {} + + void on_listener_start(container& c) override { + static char buf[1024]; + + snprintf(buf, sizeof(buf), RAW_STRING( + "scheme":"amqps", + "sasl":{ "mechanisms": "EXTERNAL" }, + "tls": { + "cert":"%s", + "key":"%s", + "ca":"%s", + "verify":true }), + client_cert, client_key, ca_cert); + + connect(c, buf); + } +}; + +} // namespace + +int main(int argc, char** argv) { + client_cert = getenv("PKCS11_CLIENT_CERT"); + client_key = getenv("PKCS11_CLIENT_KEY"); + + server_cert = getenv("PKCS11_SERVER_CERT"); + server_key = getenv("PKCS11_SERVER_KEY"); + + ca_cert = getenv("PKCS11_CA_CERT"); + + if (!client_key || !client_cert || !server_key || !server_cert || !ca_cert) { + std::cout << argv[0] << ": Environment variable configuration missing:" << std::endl; + std::cout << "\tPKCS11_CLIENT_CERT: URI of client certificate" << std::endl; + std::cout << "\tPKCS11_CLIENT_KEY: URI of client private key" << std::endl; + std::cout << "\tPKCS11_SERVER_CERT: URI of server certificate" << std::endl; + std::cout << "\tPKCS11_SERVER_KEY: URI of server private key" << std::endl; + std::cout << "\tPKCS11_CA_CERT: URI of CA certificate" << std::endl; + return SKIP_RETURN_CODE; + } + + int failed = 0; + + pn_ssl_domain_t *have_ssl = pn_ssl_domain(PN_SSL_MODE_SERVER); + + if (!have_ssl) { + std::cout << "SKIP: TLS tests, not available" << std::endl; + return SKIP_RETURN_CODE; + } + + pn_ssl_domain_free(have_ssl); + RUN_TEST(failed, test_tls_external().run()); + + return failed; +} diff --git a/cpp/testdata/certs/make_certs.sh b/cpp/testdata/certs/make_certs.sh index 0b180b6057..9c80d7f89c 100755 --- a/cpp/testdata/certs/make_certs.sh +++ b/cpp/testdata/certs/make_certs.sh @@ -18,6 +18,7 @@ keytool -storetype pkcs12 -keystore server-lh.pkcs12 -storepass server-password keytool -storetype pkcs12 -keystore server-lh.pkcs12 -storepass server-password -alias server-certificate -keypass server-password -certreq -file server-request-lh.pem keytool -storetype pkcs12 -keystore ca.pkcs12 -storepass ca-password -alias ca -keypass ca-password -gencert -rfc -validity 99999 -infile server-request-lh.pem -outfile server-certificate-lh.pem openssl pkcs12 -nocerts -passin pass:server-password -in server-lh.pkcs12 -passout pass:server-password -out server-private-key-lh.pem +openssl pkcs12 -nocerts -passin pass:server-password -in server-lh.pkcs12 -nodes -out server-private-key-lh-no-password.pem # Create a certificate request for the client certificate. Use the CA's certificate to sign it: keytool -storetype pkcs12 -keystore client.pkcs12 -storepass client-password -alias client-certificate -keypass client-password -keyalg RSA -genkey -dname "O=Client,CN=127.0.0.1" -validity 99999 diff --git a/cpp/testdata/certs/server-private-key-lh-no-password.pem b/cpp/testdata/certs/server-private-key-lh-no-password.pem new file mode 100644 index 0000000000..07ff58a92d --- /dev/null +++ b/cpp/testdata/certs/server-private-key-lh-no-password.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCVOfIlORCE9+96 +/GjwQgaNine138F/EI4QM0NkvmWfxCxUL+OQn//LCZOtsA/OpizubPrp1vThqYuy +9lHD4Mx3quNNODke6oWrQ63V4E5XYH9tn9xeUAJoiNzrdSTLkfXgJJEgqjzw55pS +S3f/6/MLrVkDqS6sGdKkcue6r5imPIZ15EUKrMvf0im5UqZDnbNVbLohrRXRSBHp +O1MI6dTbAZ+MdgbOLcC/lPZrW3hgz8LlLOmSBW+tqNTSLwQ0Hc2v7lJx78XVGZAF +swP7+03M3MubjFeEs9Ipn9HTnG2aC7p9FJ0M9MlJK5FFv2K3tFEwAId3rPcAReDq +X23/K1J7AgMBAAECggEBAJP9bXkgyJM111germtm71y7b9D84MaEwn6qeDGW6O2y +/UtYWtR4+JKBIuXjbym/f1vM1GHHff+1xwdqZNhfPieHX/iaw3s3leytJ96tnsPk +vTsYiNE3g8vrvzv7ZsxEKpVpbkv4yIsZBOCMW6uAcf6ooViSFekzisTv94Qa1MY2 +REPcpfzNQDP04szB7VeWGc1fO9bqqD28nPW4qpzJc7kSW2R7YbhqgLyvc/vAc0kt +Wps5dSs84Gc2d39IWt0Z6c/+0MUw505Pt3aYs4Q2xExjt1/oobGNyADwKPsxpmua +vqd8FcQmGOL1KQgAtJzbkw7tC+oQXuPk5DUXbVEXm9ECgYEA4kmFztVnCSWrUZSM +zooFEGp5PmmTelMNXeB10lRJSBUYw+4xBrYXsR2ikNGDqRuTJQkdfG+LNHIcoq36 +nMAJvZXwN5u7wrmNMvd+rv73iucC34/ANSWAyiQcpKnqM0rQebXc8Ra5GjEIWXB9 +KxU3RQWeBnM4FBZ/YEltT4glRd0CgYEAqNIUY4hIRQjUVLDRno8NHiReAgpVNogU +tORAjuc7oJdYMN3q+04mTNxj8VR4gHK6SiZAcILuZYkFWfC3ivPHp2pxzwm3Zslf +W/+nXGnR1rCZKSuVbPDkAyqDWZxyi96nvjaOqOF537oL9pwBQ4AL8Mfn5ppsG18K +/4urcGoBkDcCgYEA4TvtRAKFnEUyUPFbdflLMRvJsqXDdW5VT6urmr7qciUNkXf0 +tIlq65Bjz2G7ewdHXwXDo6gjFwC+H+6sFHnRODOV9sO8EAZA1QojvmtqWYe3BG9B +EaVSm+F14TB/PK6q83phgFbtx3Qmq1+cNtXXPYxpzmHA373E60Iq247YCsECgYAo +9IYju10k+kZgoWDJIZUiGdqAjjcr+oljdPhActJhXDX17PBjtQrPnKvWURLGvo55 +DJyXbvwcv8f/kMlGOWvXLpibjJTkp7etnvDgF3/joIYXmc4vVqVKK1cgNzcGvaZe +G+gyCjlB0GW0lxYrZPYAnM6igBX38e++HQkjRWRJswKBgF6JbeE/kEAsy6hSf7ww +NN2734VzNguycLWEHzVlyg1vYQogXPoFDlxrJ9G4QdzHuYQ1Bj/Qh5aG3RV0egC3 +unPDSWiY3DTlx0wGeyYc/iyTMfKIQ3d81vfjNJF0uRvdhUKA0Ubn/qx25DTxmgUW +QnhRCvGHXLFPPv2gBrtXuge9 +-----END PRIVATE KEY----- diff --git a/cpp/tests.cmake b/cpp/tests.cmake index 8c63c24b65..b00d501295 100644 --- a/cpp/tests.cmake +++ b/cpp/tests.cmake @@ -69,6 +69,16 @@ if (ENABLE_JSONCPP) set_tests_properties(cpp-connect_config_test PROPERTIES WORKING_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}") # Test data and output directories for connect_config_test file(COPY "${CMAKE_CURRENT_SOURCE_DIR}/testdata" DESTINATION "${CMAKE_CURRENT_BINARY_DIR}") + + if (NOT CMAKE_SYSTEM_NAME STREQUAL Windows) + add_executable (pkcs11_test src/pkcs11_test.cpp) + target_link_libraries (pkcs11_test qpid-proton-cpp qpid-proton-core ${PLATFORM_LIBS}) + # PKCS#11 URIs contain semicolons, which CMake would interpret as + # list sepearator, so we side step add_cpp_test and pass the env + # through as is. + add_test(NAME cpp-pkcs11_test COMMAND $) + set_tests_properties(cpp-pkcs11_test PROPERTIES SKIP_RETURN_CODE 127) + endif() endif() if (ENABLE_OPENTELEMETRYCPP) diff --git a/scripts/openssl-pkcs11.cnf b/scripts/openssl-pkcs11.cnf new file mode 100644 index 0000000000..fa4a0ecec9 --- /dev/null +++ b/scripts/openssl-pkcs11.cnf @@ -0,0 +1,22 @@ +HOME = . + +# Use this in order to automatically load providers. +openssl_conf = openssl_init + +[openssl_init] +providers = provider_sect + +[provider_sect] +default = default_sect +pkcs11 = pkcs11_sect + +[default_sect] +activate = 1 + +[pkcs11_sect] +module = $ENV::PKCS11_PROVIDER +pkcs11-module-quirks = no-operation-state no-deinit +pkcs11-module-load-behavior = $ENV::PKCS11_MODULE_LOAD_BEHAVIOR +pkcs11-module-encode-provider-uri-to-pem = true +pkcs11-module-token-pin = tclientpw +activate = 1 diff --git a/scripts/prep-pkcs11_test.sh b/scripts/prep-pkcs11_test.sh new file mode 100644 index 0000000000..f63a3abddf --- /dev/null +++ b/scripts/prep-pkcs11_test.sh @@ -0,0 +1,85 @@ +# 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. +# + +# prep-pkcs11_test.sh - Source to set up environment for pkcs11_test to run +# against a SoftHSM + +set -x + +KEYDIR="$(readlink -f cpp/testdata/certs)" + +if [ -z "$PKCS11_PROVIDER" ]; then + export PKCS11_PROVIDER=$(openssl version -m | cut -d'"' -f2)/pkcs11.so +fi + +if [ -z "$PKCS11_PROVIDER_MODULE" ]; then + export PKCS11_PROVIDER_MODULE="/usr/lib/softhsm/libsofthsm2.so" +fi + +PKCS11_PROVIDER=$(readlink -f "$PKCS11_PROVIDER") +PKCS11_PROVIDER_MODULE=$(readlink -f "$PKCS11_PROVIDER_MODULE") + +if [ ! -r "$PKCS11_PROVIDER" ]; then + echo "PKCS11_PROVIDER=$PKCS11_PROVIDER not found" + return 1 +fi + +if [ ! -r "$PKCS11_PROVIDER_MODULE" ]; then + echo "PKCS11_PROVIDER_MODULE=$PKCS11_PROVIDER_MODULE not found" + return 1 +fi + +export OPENSSL_CONF="$(readlink -f scripts/openssl-pkcs11.cnf)" +export SOFTHSM2_CONF="${XDG_RUNTIME_DIR}/qpid-proton-build/softhsm2.conf" + +softhsmtokendir="${XDG_RUNTIME_DIR}/qpid-proton-build/softhsm2-tokens" +mkdir -p "${softhsmtokendir}" + +sed -r "s;@softhsmtokendir@;${softhsmtokendir};g" scripts/softhsm2.conf.in >$SOFTHSM2_CONF + +export PKCS11_MODULE_LOAD_BEHAVIOR=late + +set -x + +softhsm2-util --delete-token --token proton-test 2>/dev/null || true +softhsm2-util --init-token --free --label proton-test --pin tclientpw --so-pin tclientpw + +pkcs11_tool () { pkcs11-tool --module=$PKCS11_PROVIDER_MODULE --token-label proton-test --pin tclientpw "$@"; } + +pkcs11_tool --module=$PKCS11_PROVIDER_MODULE --token-label proton-test --pin tclientpw -l --label tclient --delete-object --type privkey 2>/dev/null || true + +pkcs11_tool --module=$PKCS11_PROVIDER_MODULE --token-label proton-test --pin tclientpw -l --label tclient --id 2222 \ + --write-object "$KEYDIR/client-certificate.pem" --type cert --usage-sign +pkcs11_tool --module=$PKCS11_PROVIDER_MODULE --token-label proton-test --pin tclientpw -l --label tclient --id 2222 \ + --write-object "$KEYDIR/client-private-key-no-password.pem" --type privkey --usage-sign + +pkcs11_tool --module=$PKCS11_PROVIDER_MODULE --token-label proton-test --pin tclientpw -l --label tserver --id 4444 \ + --write-object "$KEYDIR/server-certificate-lh.pem" --type cert --usage-sign +pkcs11_tool --module=$PKCS11_PROVIDER_MODULE --token-label proton-test --pin tclientpw -l --label tserver --id 4444 \ + --write-object "$KEYDIR/server-private-key-lh-no-password.pem" --type privkey --usage-sign + +set +x + +# Workaround for https://github.com/latchset/pkcs11-provider/issues/419 +export PKCS11_MODULE_LOAD_BEHAVIOR=early + +export PKCS11_CLIENT_CERT="pkcs11:token=proton-test;object=tclient;type=cert" +export PKCS11_CLIENT_KEY="pkcs11:token=proton-test;object=tclient;type=private" +export PKCS11_SERVER_CERT="pkcs11:token=proton-test;object=tserver;type=cert" +export PKCS11_SERVER_KEY="pkcs11:token=proton-test;object=tserver;type=private" +export PKCS11_CA_CERT="$KEYDIR/ca-certificate.pem" diff --git a/scripts/softhsm2.conf.in b/scripts/softhsm2.conf.in new file mode 100644 index 0000000000..72a25bd3d1 --- /dev/null +++ b/scripts/softhsm2.conf.in @@ -0,0 +1,16 @@ +# SoftHSM v2 configuration file + +directories.tokendir = @softhsmtokendir@ +objectstore.backend = file + +# ERROR, WARNING, INFO, DEBUG +log.level = ERROR + +# If CKF_REMOVABLE_DEVICE flag should be set +slots.removable = false + +# Enable and disable PKCS#11 mechanisms using slots.mechanisms. +slots.mechanisms = ALL + +# If the library should reset the state on fork +library.reset_on_fork = false