Mutual TLS parsing change at runtime
Redfish AccountService[1] defines methods for selecting how to map a
certificate CommonName attribute to a user. These are intended to be a
patch parameter.
This commit implements the Redfish defined schemas; The parsing mode is
stored in the bmcweb persistent configuration file as an integer enum,
with Mapping to the Redfish schema.
To handle OEM specific parsing modes, an enum value of 100+ is defined
to allow the additional OEM parameters. Unfortunately, Redfish doesn't
have a way to represent these today, so those modes are currently not
selectable at runtime.
Now that things are runtime selectable, this obsoletes the option
mutual-tls-common-name-parsing, as it is not longer required at compile
time.
Tested:
GET /redfish/v1/AccountService
returns MultiFactorAuth/ClientCertificate/CertificateMappingAttribute
PATCH /redfish/v1/AccountService
```
{"MultiFactorAuth": {"ClientCertificate": {"CertificateMappingAttribute":"CommonName"}}}
```
Returns 200
[1] https://github.com/DMTF/Redfish-Publications/blob/5b217908b5378b24e4f390c063427d7a707cd308/csdl/AccountService_v1.xml#L1631
Change-Id: I67db0dfa5245a9da973320aab666d12dbd9229e4
Signed-off-by: Ed Tanous <ed@tanous.net>
diff --git a/config/meson.build b/config/meson.build
index b500dee..63e7acb 100644
--- a/config/meson.build
+++ b/config/meson.build
@@ -18,6 +18,7 @@
'insecure-push-style-notification',
'insecure-tftp-update',
'kvm',
+ 'meta-tls-common-name-parsing',
'mutual-tls-auth',
'redfish-aggregation',
'redfish-allow-deprecated-power-thermal',
@@ -41,7 +42,7 @@
string_options = [
'dns-resolver',
- 'mutual-tls-common-name-parsing',
+ 'mutual-tls-common-name-parsing-default',
'redfish-manager-uri-name',
'redfish-system-uri-name',
]
diff --git a/http/mutual_tls.hpp b/http/mutual_tls.hpp
index 5392549..5acc87a 100644
--- a/http/mutual_tls.hpp
+++ b/http/mutual_tls.hpp
@@ -16,6 +16,38 @@
#include <memory>
#include <span>
+inline std::string getUsernameFromCommonName(std::string_view commonName)
+{
+ const persistent_data::AuthConfigMethods& authMethodsConfig =
+ persistent_data::SessionStore::getInstance().getAuthMethodsConfig();
+ switch (authMethodsConfig.mTLSCommonNameParsingMode)
+ {
+ case persistent_data::MTLSCommonNameParseMode::Invalid:
+ case persistent_data::MTLSCommonNameParseMode::Whole:
+ case persistent_data::MTLSCommonNameParseMode::UserPrincipalName:
+ {
+ // Not yet supported
+ return "";
+ }
+ case persistent_data::MTLSCommonNameParseMode::CommonName:
+ {
+ return std::string{commonName};
+ }
+ case persistent_data::MTLSCommonNameParseMode::Meta:
+ {
+ // Meta Inc. CommonName parsing
+ std::optional<std::string_view> sslUserMeta =
+ mtlsMetaParseSslUser(commonName);
+ if (!sslUserMeta)
+ {
+ return "";
+ }
+ return std::string{*sslUserMeta};
+ }
+ }
+ return "";
+}
+
inline std::shared_ptr<persistent_data::UserSession>
verifyMtlsUser(const boost::asio::ip::address& clientIp,
boost::asio::ssl::verify_context& ctx)
@@ -71,39 +103,27 @@
return nullptr;
}
- std::string sslUser;
+ std::string commonName;
// Extract username contained in CommonName
- sslUser.resize(256, '\0');
+ commonName.resize(256, '\0');
- int status = X509_NAME_get_text_by_NID(X509_get_subject_name(peerCert),
- NID_commonName, sslUser.data(),
- static_cast<int>(sslUser.size()));
-
- if (status == -1)
+ int length = X509_NAME_get_text_by_NID(X509_get_subject_name(peerCert),
+ NID_commonName, commonName.data(),
+ static_cast<int>(commonName.size()));
+ if (length <= 0)
{
- BMCWEB_LOG_DEBUG("TLS cannot get username to create session");
+ BMCWEB_LOG_DEBUG("TLS cannot get common name to create session");
return nullptr;
}
- size_t lastChar = sslUser.find('\0');
- if (lastChar == std::string::npos || lastChar == 0)
+ commonName.resize(static_cast<size_t>(length));
+ std::string sslUser = getUsernameFromCommonName(commonName);
+ if (sslUser.empty())
{
- BMCWEB_LOG_DEBUG("Invalid TLS user name");
+ BMCWEB_LOG_WARNING("Failed to get user from common name {}",
+ commonName);
return nullptr;
}
- sslUser.resize(lastChar);
-
- // Meta Inc. CommonName parsing
- if constexpr (BMCWEB_MUTUAL_TLS_COMMON_NAME_PARSING == "meta")
- {
- std::optional<std::string_view> sslUserMeta =
- mtlsMetaParseSslUser(sslUser);
- if (!sslUserMeta)
- {
- return nullptr;
- }
- sslUser = *sslUserMeta;
- }
std::string unsupportedClientId;
return persistent_data::SessionStore::getInstance().generateUserSession(
diff --git a/include/persistent_data.hpp b/include/persistent_data.hpp
index 3bd256d..fff08d3 100644
--- a/include/persistent_data.hpp
+++ b/include/persistent_data.hpp
@@ -214,7 +214,8 @@
std::filesystem::perms::owner_write |
std::filesystem::perms::group_read;
std::filesystem::permissions(filename, permission);
- const auto& c = SessionStore::getInstance().getAuthMethodsConfig();
+ const AuthConfigMethods& c =
+ SessionStore::getInstance().getAuthMethodsConfig();
const auto& eventServiceConfig =
EventServiceStore::getInstance().getEventServiceConfig();
nlohmann::json::object_t data;
@@ -225,6 +226,8 @@
authConfig["SessionToken"] = c.sessionToken;
authConfig["BasicAuth"] = c.basic;
authConfig["TLS"] = c.tls;
+ authConfig["TLSCommonNameParseMode"] =
+ static_cast<int>(c.mTLSCommonNameParsingMode);
nlohmann::json& eventserviceConfig = data["eventservice_config"];
eventserviceConfig["ServiceEnabled"] = eventServiceConfig.enabled;
diff --git a/include/sessions.hpp b/include/sessions.hpp
index 5621fff..9c064ee 100644
--- a/include/sessions.hpp
+++ b/include/sessions.hpp
@@ -134,6 +134,49 @@
}
};
+enum class MTLSCommonNameParseMode
+{
+ Invalid = 0,
+ // This section approximately matches Redfish AccountService
+ // CertificateMappingAttribute, plus bmcweb defined OEM ones.
+ // Note, IDs in this enum must be maintained between versions, as they are
+ // persisted to disk
+ Whole = 1,
+ CommonName = 2,
+ UserPrincipalName = 3,
+
+ // Intentional gap for future DMTF-defined enums
+
+ // OEM parsing modes for various OEMs
+ Meta = 100,
+};
+
+inline MTLSCommonNameParseMode getMTLSCommonNameParseMode(std::string_view name)
+{
+ if (name == "CommonName")
+ {
+ return MTLSCommonNameParseMode::CommonName;
+ }
+ if (name == "Whole")
+ {
+ // Not yet supported
+ // return MTLSCommonNameParseMode::Whole;
+ }
+ if (name == "UserPrincipalName")
+ {
+ // Not yet supported
+ // return MTLSCommonNameParseMode::UserPrincipalName;
+ }
+ if constexpr (BMCWEB_META_TLS_COMMON_NAME_PARSING)
+ {
+ if (name == "Meta")
+ {
+ return MTLSCommonNameParseMode::Meta;
+ }
+ }
+ return MTLSCommonNameParseMode::Invalid;
+}
+
struct AuthConfigMethods
{
bool basic = BMCWEB_BASIC_AUTH;
@@ -142,35 +185,56 @@
bool cookie = BMCWEB_COOKIE_AUTH;
bool tls = BMCWEB_MUTUAL_TLS_AUTH;
+ MTLSCommonNameParseMode mTLSCommonNameParsingMode =
+ getMTLSCommonNameParseMode(
+ BMCWEB_MUTUAL_TLS_COMMON_NAME_PARSING_DEFAULT);
+
void fromJson(const nlohmann::json::object_t& j)
{
for (const auto& element : j)
{
const bool* value = element.second.get_ptr<const bool*>();
- if (value == nullptr)
+ if (value != nullptr)
{
- continue;
+ if (element.first == "XToken")
+ {
+ xtoken = *value;
+ }
+ else if (element.first == "Cookie")
+ {
+ cookie = *value;
+ }
+ else if (element.first == "SessionToken")
+ {
+ sessionToken = *value;
+ }
+ else if (element.first == "BasicAuth")
+ {
+ basic = *value;
+ }
+ else if (element.first == "TLS")
+ {
+ tls = *value;
+ }
}
-
- if (element.first == "XToken")
+ const uint64_t* intValue =
+ element.second.get_ptr<const uint64_t*>();
+ if (intValue != nullptr)
{
- xtoken = *value;
- }
- else if (element.first == "Cookie")
- {
- cookie = *value;
- }
- else if (element.first == "SessionToken")
- {
- sessionToken = *value;
- }
- else if (element.first == "BasicAuth")
- {
- basic = *value;
- }
- else if (element.first == "TLS")
- {
- tls = *value;
+ if (element.first == "MTLSCommonNameParseMode")
+ {
+ if (*intValue <= 2 || *intValue == 100)
+ {
+ mTLSCommonNameParsingMode =
+ static_cast<MTLSCommonNameParseMode>(*intValue);
+ }
+ else
+ {
+ BMCWEB_LOG_ERROR(
+ "Json value of {} was out of range of the enum. Ignoring",
+ *intValue);
+ }
+ }
}
}
}
diff --git a/meson.options b/meson.options
index 205ce71..f1adf15 100644
--- a/meson.options
+++ b/meson.options
@@ -191,20 +191,25 @@
)
option(
- 'mutual-tls-common-name-parsing',
+ 'mutual-tls-common-name-parsing-default',
type: 'combo',
- choices: ['username', 'meta'],
- value: 'username',
- description: '''Sets logic to map the Subject Common Name field to a user
- in client TLS certificates.
- - username: Use the Subject CN field as a BMC username
- (default)
- - meta: Parses the Subject CN in the format used by
+ choices: ['CommonName', 'Whole', 'UserPrincipalName', 'Meta'],
+ description: '''
+ Parses the Subject CN in the format used by
Meta Inc (see mutual_tls_meta.cpp for details)
''',
)
option(
+ 'meta-tls-common-name-parsing',
+ type: 'feature',
+ description: '''
+ Allows parsing the Subject CN TLS certificate in the format used by
+ Meta Inc (see mutual_tls_meta.cpp for details)
+ ''',
+)
+
+option(
'ibm-management-console',
type: 'feature',
value: 'disabled',
diff --git a/redfish-core/lib/account_service.hpp b/redfish-core/lib/account_service.hpp
index 05cfbe7..b3b848f 100644
--- a/redfish-core/lib/account_service.hpp
+++ b/redfish-core/lib/account_service.hpp
@@ -1257,6 +1257,45 @@
getClientCertificates(asyncResp, "/Members"_json_pointer);
}
+using account_service::CertificateMappingAttribute;
+using persistent_data::MTLSCommonNameParseMode;
+inline CertificateMappingAttribute
+ getCertificateMapping(MTLSCommonNameParseMode parse)
+{
+ switch (parse)
+ {
+ case MTLSCommonNameParseMode::CommonName:
+ {
+ return CertificateMappingAttribute::CommonName;
+ }
+ break;
+ case MTLSCommonNameParseMode::Whole:
+ {
+ return CertificateMappingAttribute::Whole;
+ }
+ break;
+ case MTLSCommonNameParseMode::UserPrincipalName:
+ {
+ return CertificateMappingAttribute::UserPrincipalName;
+ }
+ break;
+
+ case MTLSCommonNameParseMode::Meta:
+ {
+ if constexpr (BMCWEB_META_TLS_COMMON_NAME_PARSING)
+ {
+ return CertificateMappingAttribute::CommonName;
+ }
+ }
+ break;
+ default:
+ {
+ return CertificateMappingAttribute::Invalid;
+ }
+ break;
+ }
+}
+
inline void
handleAccountServiceGet(App& app, const crow::Request& req,
const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
@@ -1301,8 +1340,19 @@
nlohmann::json::object_t clientCertificate;
clientCertificate["Enabled"] = authMethodsConfig.tls;
clientCertificate["RespondToUnauthenticatedClients"] = true;
- clientCertificate["CertificateMappingAttribute"] =
- account_service::CertificateMappingAttribute::CommonName;
+
+ using account_service::CertificateMappingAttribute;
+
+ CertificateMappingAttribute mapping =
+ getCertificateMapping(authMethodsConfig.mTLSCommonNameParsingMode);
+ if (mapping == CertificateMappingAttribute::Invalid)
+ {
+ messages::internalError(asyncResp->res);
+ }
+ else
+ {
+ clientCertificate["CertificateMappingAttribute"] = mapping;
+ }
nlohmann::json::object_t certificates;
certificates["@odata.id"] =
"/redfish/v1/AccountService/MultiFactorAuth/ClientCertificate/Certificates";
@@ -1400,6 +1450,24 @@
getLDAPConfigData("ActiveDirectory", callback);
}
+inline void
+ handleCertificateMappingAttributePatch(crow::Response& res,
+ const std::string& certMapAttribute)
+{
+ MTLSCommonNameParseMode parseMode =
+ persistent_data::getMTLSCommonNameParseMode(certMapAttribute);
+ if (parseMode == MTLSCommonNameParseMode::Invalid)
+ {
+ messages::propertyValueNotInList(res, "CertificateMappingAttribute",
+ certMapAttribute);
+ return;
+ }
+
+ persistent_data::AuthConfigMethods& authMethodsConfig =
+ persistent_data::SessionStore::getInstance().getAuthMethodsConfig();
+ authMethodsConfig.mTLSCommonNameParsingMode = parseMode;
+}
+
inline void handleAccountServicePatch(
App& app, const crow::Request& req,
const std::shared_ptr<bmcweb::AsyncResp>& asyncResp)
@@ -1413,9 +1481,11 @@
std::optional<uint8_t> minPasswordLength;
std::optional<uint16_t> maxPasswordLength;
LdapPatchParams ldapObject;
+ std::optional<std::string> certificateMappingAttribute;
LdapPatchParams activeDirectoryObject;
AuthMethods auth;
std::optional<std::string> httpBasicAuth;
+
// clang-format off
if (!json_util::readJsonPatch(
req, asyncResp->res,
@@ -1430,6 +1500,7 @@
"ActiveDirectory/RemoteRoleMapping", activeDirectoryObject.remoteRoleMapData,
"ActiveDirectory/ServiceAddresses", activeDirectoryObject.serviceAddressList,
"ActiveDirectory/ServiceEnabled", activeDirectoryObject.serviceEnabled,
+ "MultiFactorAuth/ClientCertificate/CertificateMappingAttribute", certificateMappingAttribute,
"LDAP/Authentication/AuthenticationType", ldapObject.authType,
"LDAP/Authentication/Password", ldapObject.password,
"LDAP/Authentication/Username", ldapObject.userName,
@@ -1469,6 +1540,12 @@
}
}
+ if (certificateMappingAttribute)
+ {
+ handleCertificateMappingAttributePatch(asyncResp->res,
+ *certificateMappingAttribute);
+ }
+
if (minPasswordLength)
{
setDbusProperty(