Skip to content

Commit

Permalink
HSDS writer and reader requests are unauthenticated
Browse files Browse the repository at this point in the history
Problem
-------

The API's exposed by the HSDS do not authenticate callers.

Solution
--------

Require a JWT and ensure that the caller is same as the DPM
application of the writer/reader.

Technically, the API verifies the JWT using the certificate from the
DPM.  It then checks that the principal is the same as for the
credentials used to download DDS security documents from the DPM.

Note
----

TLS is not supported (yet).  Users should still run the writer and
reader with a reverse proxy that can provide TLS termination.
  • Loading branch information
jrw972 committed Feb 26, 2024
1 parent d11d555 commit f4c2c61
Show file tree
Hide file tree
Showing 21 changed files with 6,097 additions and 34 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
shell: bash
run: |
cd OpenDDS
./configure --ace-github-latest --security
./configure --ace-github-latest --std=c++17 --security
- name: get OpenDDS commit
shell: bash
run: |
Expand Down Expand Up @@ -137,7 +137,7 @@ jobs:
with:
repository: etr/libhttpserver
path: libhttpserver
ref: 0.18.2
ref: 0.19.0
- name: install libhttpserver
shell: bash
run: |
Expand Down
7 changes: 6 additions & 1 deletion CommunitySecurityPlugin/CommunitySecurityPlugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,12 @@ class AccessControl : public OpenDDS::Security::AccessControlBuiltInImpl {
return OpenDDS::Security::CommonUtilities::set_security_error(ex, -1, 0, "CommunityAccessControl::common_check: Subject name does not contain a SN");
}

if (pos->second != dpmgid.in()) {
const std::map<std::string, std::string>::const_iterator pos2 = pos->find("SN");
if (pos2 == pos->end()) {
return OpenDDS::Security::CommonUtilities::set_security_error(ex, -1, 0, "CommunityAccessControl::common_check: Subject name does not contain a DPMGID");
}

if (pos2->second != dpmgid.in()) {
return OpenDDS::Security::CommonUtilities::set_security_error(ex, -1, 0, "CommunityAccessControl::common_check: dpmgid from sample does not match SN from subject name");
}

Expand Down
35 changes: 33 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,47 @@
# Copyright 2023 CommunityUtils Authors

ARG BASIS=ghcr.io/opendds/opendds:master
ARG BASIS=ubuntu:focal
FROM $BASIS

ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
cmake \
curl \
g++ \
make \
libxerces-c-dev \
libssl-dev \
perl-base \
perl-modules \
git

ADD . /opt/OpenDDS

ARG ACE_CONFIG_OPTION="--doc-group"
RUN cd /opt/OpenDDS && \
./configure --prefix=/usr/local --std=c++17 --security ${ACE_CONFIG_OPTION} && \
./tools/scripts/show_build_config.pl && \
make && \
make install && \
ldconfig && \
. /opt/OpenDDS/setenv.sh && \
cp -a ${MPC_ROOT} /usr/local/share/MPC

ENV ACE_ROOT=/usr/local/share/ace \
TAO_ROOT=/usr/local/share/tao \
DDS_ROOT=/usr/local/share/dds \
MPC_ROOT=/usr/local/share/MPC

WORKDIR /opt/workspace

ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y && apt-get install -y wget automake autoconf libtool libmicrohttpd-dev

RUN wget https://github.com/curl/curl/releases/download/curl-7_87_0/curl-7.87.0.tar.gz && tar xzf curl-7.87.0.tar.gz && cd curl-7.87.0 && ./configure --with-openssl && make && make install

RUN git clone --depth 1 --branch 3.1 --single-branch https://github.com/JosephP91/curlcpp.git && cd curlcpp && mkdir build && cd build && cmake .. -DBUILD_SHARED_LIBS=SHARED && make && make install

RUN git clone --depth 1 --branch 0.18.2 --single-branch https://github.com/etr/libhttpserver.git && cd libhttpserver && ./bootstrap && mkdir build && cd build && ../configure && make && make install
RUN git clone --depth 1 --branch 0.19.0 --single-branch https://github.com/etr/libhttpserver.git && cd libhttpserver && ./bootstrap && mkdir build && cd build && ../configure && make && make install

ADD . /opt/CommunityUtils
ENV COMMUNITY_UTILS_ROOT=/opt/CommunityUtils
Expand Down
75 changes: 56 additions & 19 deletions HSDS/Common/Application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -331,25 +331,62 @@ Application::download_security_documents()
return ret;
}

std::string key_pair;
ret = download_security_document(easy, dpm_url_ + "/api/applications/key_pair?nonce=" + nonce, key_pair);
if (ret != DDS::RETCODE_OK) {
return ret;
{
std::string key_pair;
ret = download_security_document(easy, dpm_url_ + "/api/applications/key_pair?nonce=" + nonce, key_pair);
if (ret != DDS::RETCODE_OK) {
return ret;
}

rapidjson::Document document;
document.Parse(key_pair.c_str());

if (document.IsObject() &&
document.HasMember("public") &&
document["public"].IsString() &&
document.HasMember("private") &&
document["private"].IsString()) {
public_key_ = document["public"].GetString();
private_key_ = document["private"].GetString();
} else {
ACE_ERROR((LM_ERROR, "ERROR: Application::download_security_documents: Could not extract public and private key\n"));
return DDS::RETCODE_ERROR;
}
}

rapidjson::Document document;
document.Parse(key_pair.c_str());

if (document.IsObject() &&
document.HasMember("public") &&
document["public"].IsString() &&
document.HasMember("private") &&
document["private"].IsString()) {
public_key_ = document["public"].GetString();
private_key_ = document["private"].GetString();
} else {
ACE_ERROR((LM_ERROR, "ERROR: Application::download_security_documents: Could not extract public and private key\n"));
return DDS::RETCODE_ERROR;
{
verifier_.with_subject(dpm_application_id_);

std::string public_keys;
ret = download_security_document(easy, dpm_url_ + "/auth/public_keys", public_keys);
if (ret != DDS::RETCODE_OK) {
return ret;
}

bool have_key = false;
rapidjson::Document document;
document.Parse(public_keys.c_str());
if (document.IsArray()) {
for (rapidjson::SizeType idx = 0; idx != document.Size(); ++idx) {
if (document[idx].IsObject() &&
document[idx].HasMember("public") &&
document[idx]["public"].IsString()) {
verifier_.allow_algorithm(jwt::algorithm::rs256(document[idx]["public"].GetString()));
have_key = true;
} else {
ACE_ERROR((LM_ERROR, "ERROR: Application::download_security_documents: DPM public key is not a string\n"));
return DDS::RETCODE_ERROR;
}
}
} else {
ACE_ERROR((LM_ERROR, "ERROR: Application::download_security_documents: Could not extract DPM public keys\n"));
return DDS::RETCODE_ERROR;
}

if (!have_key) {
ACE_ERROR((LM_ERROR, "ERROR: Application::download_security_documents: No DPM public keys\n"));
return DDS::RETCODE_ERROR;
}
}

return DDS::RETCODE_OK;
Expand Down Expand Up @@ -424,8 +461,8 @@ Application::setup_discovery()
OpenDDS::DCPS::RcHandle<OpenDDS::RTPS::RtpsDiscovery> discovery = OpenDDS::DCPS::make_rch<OpenDDS::RTPS::RtpsDiscovery>("RtpsDiscovery");
discovery->config()->use_rtps_relay(true);
discovery->config()->rtps_relay_only(rtps_relay_only_);
discovery->config()->spdp_rtps_relay_address(ACE_INET_Addr(spdp_rtps_relay_address_.c_str()));
discovery->config()->sedp_rtps_relay_address(ACE_INET_Addr(sedp_rtps_relay_address_.c_str()));
discovery->config()->spdp_rtps_relay_address(OpenDDS::DCPS::NetworkAddress(spdp_rtps_relay_address_.c_str()));
discovery->config()->sedp_rtps_relay_address(OpenDDS::DCPS::NetworkAddress(sedp_rtps_relay_address_.c_str()));
discovery->config()->sedp_max_message_size(1400);
discovery->config()->secure_participant_user_data(true);
discovery->config()->sedp_responsive_mode(true);
Expand Down
6 changes: 6 additions & 0 deletions HSDS/Common/Application.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#include <dds/DCPS/DCPS_Utils.h>
#include <dds/DCPS/transport/framework/TransportConfig_rch.h>

#include "jwt-cpp/jwt.h"

template <typename T>
struct Unit {
Unit(const std::string& key,
Expand Down Expand Up @@ -41,6 +43,7 @@ class HSDS_Common_Export Application {
: domain_id_(1)
, http_port_(8080)
, create_writers_(true)
, verifier_(jwt::verify().with_issuer("dds-permissions-manager"))
, rtps_relay_only_(true)
, enable_http_log_access_(true)
, enable_observer_(true)
Expand Down Expand Up @@ -440,6 +443,8 @@ class HSDS_Common_Export Application {

ACE_Thread_Mutex& get_mutex() { return mutex_; }


const jwt::verifier<jwt::default_clock, jwt::traits::kazuho_picojson>& verifier() const { return verifier_; }
private:
std::string dpm_url_;
std::string dpm_gid_;
Expand All @@ -463,6 +468,7 @@ class HSDS_Common_Export Application {
std::string public_key_;
std::string private_key_;
std::string permissions_;
jwt::verifier<jwt::default_clock, jwt::traits::kazuho_picojson> verifier_;

bool rtps_relay_only_;
std::string spdp_rtps_relay_address_;
Expand Down
1 change: 1 addition & 0 deletions HSDS/Common/HSDSCommon.mpc
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ project: opendds_cxx11, dcps_rtps_udp, CommunitySecurityPlugin, install {
lit_libs += curlcpp
idlflags += -Wb,export_include=HSDS_Common_export.h -Wb,export_macro=HSDS_Common_Export -SS
dcps_ts_flags += -Gxtypes-complete -Gequality -Wb,export_include=HSDS_Common_export.h -Wb,export_macro=HSDS_Common_Export
includes += .

TypeSupport_Files {
HSDS3.idl
Expand Down
51 changes: 41 additions & 10 deletions HSDS/Common/HsdsResource.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ class HsdsElementResource : public UnitResourceBase<T> {
ws.register_resource(application.unit<T>().endpoint + "/{dpmgid}/{id}", this);
}

const std::shared_ptr<httpserver::http_response> render_PUT(const httpserver::http_request& request) {
std::shared_ptr<httpserver::http_response> render_PUT(const httpserver::http_request& request) {
if (!ResourceBase::is_authorized(request)) {
return this->respond(ErrorResponse::make_unauthorized());
}

// Extract the dpmgid and id from the URL.
const std::string dpmgid = request.get_arg("dpmgid");
const std::string id = request.get_arg("id");
Expand All @@ -40,7 +44,8 @@ class HsdsElementResource : public UnitResourceBase<T> {
}

// Parse the input.
rapidjson::StringStream ss(request.get_content().c_str());
const std::string s(request.get_content());
rapidjson::StringStream ss(s.c_str());
T element;
if (!OpenDDS::DCPS::from_json(element, ss)) {
ACE_ERROR((LM_NOTICE, "NOTICE: HsdsElementResource::render_PUT: failed to parse input json!!\n"));
Expand Down Expand Up @@ -73,7 +78,11 @@ class HsdsElementResource : public UnitResourceBase<T> {
return this->respond_with_json(OpenDDS::DCPS::to_json(element));
}

const std::shared_ptr<httpserver::http_response> render_GET(const httpserver::http_request& request) {
std::shared_ptr<httpserver::http_response> render_GET(const httpserver::http_request& request) {
if (!ResourceBase::is_authorized(request)) {
return this->respond(ErrorResponse::make_unauthorized());
}

// Extract the dpmgid and id from the URL.
const std::string dpmgid = request.get_arg("dpmgid");
const std::string id = request.get_arg("id");
Expand All @@ -100,7 +109,11 @@ class HsdsElementResource : public UnitResourceBase<T> {
return this->respond_with_json(OpenDDS::DCPS::to_json(*pos));
}

const std::shared_ptr<httpserver::http_response> render_DELETE(const httpserver::http_request& request) {
std::shared_ptr<httpserver::http_response> render_DELETE(const httpserver::http_request& request) {
if (!ResourceBase::is_authorized(request)) {
return this->respond(ErrorResponse::make_unauthorized());
}

// Extract the dpmgid and id from the URL.
const std::string dpmgid = request.get_arg("dpmgid");
const std::string id = request.get_arg("id");
Expand Down Expand Up @@ -150,12 +163,17 @@ class HsdsCollectionResource : public UnitResourceBase<T> {
ws.register_resource(application.unit<T>().endpoint, this);
}

const std::shared_ptr<httpserver::http_response> render_PUT(const httpserver::http_request& request) {
std::shared_ptr<httpserver::http_response> render_PUT(const httpserver::http_request& request) {
if (!ResourceBase::is_authorized(request)) {
return this->respond(ErrorResponse::make_unauthorized());
}

typedef typename UnitResourceBase<T>::ContainerType ContainerType;
ContainerType new_container;

// Parse the input.
rapidjson::StringStream ss(request.get_content().c_str());
const std::string s(request.get_content());
rapidjson::StringStream ss(s.c_str());
OpenDDS::DCPS::JsonValueReader<rapidjson::StringStream> jvr(ss);
if (!jvr.begin_sequence()) {
ACE_ERROR((LM_NOTICE, "NOTICE: HsdsCollectionResource::render_PUT: begin_sequence failed!\n"));
Expand Down Expand Up @@ -271,11 +289,16 @@ class HsdsCollectionResource : public UnitResourceBase<T> {
return this->respond_with_no_content();
}

const std::shared_ptr<httpserver::http_response> render_POST(const httpserver::http_request& request) {
std::shared_ptr<httpserver::http_response> render_POST(const httpserver::http_request& request) {
if (!ResourceBase::is_authorized(request)) {
return this->respond(ErrorResponse::make_unauthorized());
}

std::vector<T> list;

// Parse the input.
rapidjson::StringStream ss(request.get_content().c_str());
const std::string s(request.get_content());
rapidjson::StringStream ss(s.c_str());
OpenDDS::DCPS::JsonValueReader<rapidjson::StringStream> jvr(ss);
if (!jvr.begin_sequence()) {
ACE_ERROR((LM_NOTICE, "NOTICE: HsdsCollectionResource::render_POST: begin_sequence failed!\n"));
Expand Down Expand Up @@ -321,7 +344,11 @@ class HsdsCollectionResource : public UnitResourceBase<T> {
return this->respond_with_no_content();
}

const std::shared_ptr<httpserver::http_response> render_GET(const httpserver::http_request& request) {
std::shared_ptr<httpserver::http_response> render_GET(const httpserver::http_request& request) {
if (!ResourceBase::is_authorized(request)) {
return this->respond(ErrorResponse::make_unauthorized());
}

size_t offset = 0;
size_t count = this->unit_.container.size();

Expand Down Expand Up @@ -362,7 +389,11 @@ class HsdsCollectionResource : public UnitResourceBase<T> {
return response;
}

const std::shared_ptr<httpserver::http_response> render_DELETE(const httpserver::http_request&) {
std::shared_ptr<httpserver::http_response> render_DELETE(const httpserver::http_request& request) {
if (!ResourceBase::is_authorized(request)) {
return this->respond(ErrorResponse::make_unauthorized());
}

ACE_GUARD_RETURN(ACE_Thread_Mutex, g, this->application_.get_mutex(),
this->respond(ErrorResponse::make_internal_server_error()));
// Unregister.
Expand Down
1 change: 1 addition & 0 deletions HSDS/Common/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
jwt-cpp and picojson are from https://github.com/Thalhammer/jwt-cpp
32 changes: 32 additions & 0 deletions HSDS/Common/ResourceBase.h
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ class ErrorResponse {
, message_("Bad Request")
{}

static ErrorResponse make_unauthorized()
{
return ErrorResponse(httpserver::http::http_utils::http_unauthorized, "Unauthorized");
}

static ErrorResponse make_bad_request(const std::string& message)
{
return ErrorResponse(httpserver::http::http_utils::http_bad_request, message);
Expand Down Expand Up @@ -61,6 +66,33 @@ class ResourceBase : public httpserver::http_resource {

virtual void add_headers(std::shared_ptr<httpserver::http_response>) const {}

bool is_authorized(const httpserver::http_request& request) const
{
const auto auth = request.get_header("Authorization");

const auto scheme_term = auth.find(' ', 0);
if (scheme_term == std::string::npos) {
ACE_ERROR((LM_NOTICE, "NOTICE: ResourceBase::is_authorized: can't find JWT\n"));
return false;
}
if (auth.substr(0, scheme_term) != "Bearer") {
ACE_ERROR((LM_NOTICE, "NOTICE: ResourceBase::is_authorized: can't find JWT\n"));
return false;
}

const std::string token(auth.substr(scheme_term + 1));
const auto decoded = jwt::decode(token);
std::error_code ec;
application_.verifier().verify(decoded, ec);

if (ec) {
ACE_ERROR((LM_NOTICE, "NOTICE: ResourceBase::is_authorized: JWT could not be verified!\n"));
return false;
}

return true;
}

// TODO: Could we have done this in IDL?
bool check_input(ErrorResponse& err,
const HSDS3::Accessibility& value) const
Expand Down
Loading

0 comments on commit f4c2c61

Please sign in to comment.