blob: dfabc97e07d615cab971ce49f28ba3a1501bdc77 [file] [log] [blame]
//========================================================================
//
// DistinguishedNameParser.h
//
// This file is licensed under the GPLv2 or later
//
// Copyright 2002 g10 Code GmbH
// Copyright 2004 Klarälvdalens Datakonsult AB
// Copyright 2021 g10 Code GmbH
// Copyright 2023 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk>
//
// Derived from libkleopatra (KDE key management library) dn.cpp
//
//========================================================================
#ifndef DISTINGUISHEDNAMEPARSER_H
#define DISTINGUISHEDNAMEPARSER_H
#include <vector>
#include <string>
#include <utility>
#include <optional>
#include <algorithm>
namespace DN {
namespace detail {
inline std::string_view removeLeadingSpaces(std::string_view view)
{
auto pos = view.find_first_not_of(' ');
if (pos > view.size()) {
return {};
}
return view.substr(pos);
}
inline std::string_view removeTrailingSpaces(std::string_view view)
{
auto pos = view.find_last_not_of(' ');
if (pos > view.size()) {
return {};
}
return view.substr(0, pos + 1);
}
inline unsigned char xtoi(unsigned char c)
{
if (c <= '9') {
return c - '0';
}
if (c <= 'F') {
return c - 'A' + 10;
}
return c < 'a' + 10;
}
inline unsigned char xtoi(unsigned char first, unsigned char second)
{
return 16 * xtoi(first) + xtoi(second);
}
// Parses a hex string into actual content
inline std::optional<std::string> parseHexString(std::string_view view)
{
auto size = view.size();
if (size == 0 || (size % 2 == 1)) {
return std::nullopt;
}
// It is only supposed to be called with actual hex strings
// but this is just to be extra sure
auto endHex = view.find_first_not_of("1234567890abcdefABCDEF");
if (endHex != std::string_view::npos) {
return {};
}
std::string result;
result.reserve(size / 2);
for (size_t i = 0; i < (view.size() - 1); i += 2) {
result.push_back(xtoi(view[i], view[i + 1]));
}
return result;
}
static const std::vector<std::pair<std::string_view, std::string_view>> oidmap = {
// clang-format off
// keep them ordered by oid:
{"NameDistinguisher", "0.2.262.1.10.7.20" },
{"EMAIL", "1.2.840.113549.1.9.1"},
{"CN", "2.5.4.3" },
{"SN", "2.5.4.4" },
{"SerialNumber", "2.5.4.5" },
{"T", "2.5.4.12" },
{"D", "2.5.4.13" },
{"BC", "2.5.4.15" },
{"ADDR", "2.5.4.16" },
{"PC", "2.5.4.17" },
{"GN", "2.5.4.42" },
{"Pseudo", "2.5.4.65" },
// clang-format on
};
static std::string_view attributeNameForOID(std::string_view oid)
{
if (oid.substr(0, 4) == std::string_view { "OID." } || oid.substr(0, 4) == std::string_view { "oid." }) { // c++20 has starts_with. we don't have that yet.
oid.remove_prefix(4);
}
for (const auto &m : oidmap) {
if (oid == m.second) {
return m.first;
}
}
return {};
}
/* Parse a DN and return an array-ized one. This is not a validating
parser and it does not support any old-stylish syntax; gpgme is
expected to return only rfc2253 compatible strings. */
static std::pair<std::optional<std::string_view>, std::pair<std::string, std::string>> parse_dn_part(std::string_view stringv)
{
std::pair<std::string, std::string> dnPair;
auto separatorPos = stringv.find_first_of('=');
if (separatorPos == 0 || separatorPos == std::string_view::npos) {
return {}; /* empty key */
}
std::string_view key = stringv.substr(0, separatorPos);
key = removeTrailingSpaces(key);
// map OIDs to their names:
if (auto name = attributeNameForOID(key); !name.empty()) {
key = name;
}
dnPair.first = std::string { key };
stringv = removeLeadingSpaces(stringv.substr(separatorPos + 1));
if (stringv.empty()) {
return {};
}
if (stringv.front() == '#') {
/* hexstring */
stringv.remove_prefix(1);
auto endHex = stringv.find_first_not_of("1234567890abcdefABCDEF");
if (!endHex || (endHex % 2 == 1)) {
return {}; /* empty or odd number of digits */
}
auto value = parseHexString(stringv.substr(0, endHex));
if (!value.has_value()) {
return {};
}
stringv = stringv.substr(endHex);
dnPair.second = value.value();
} else if (stringv.front() == '"') {
stringv.remove_prefix(1);
std::string value;
bool stop = false;
while (!stringv.empty() && !stop) {
switch (stringv.front()) {
case '\\': {
if (stringv.size() < 2) {
return {};
}
if (stringv[1] == '"') {
value.push_back('"');
stringv.remove_prefix(2);
} else {
// it is a bit unclear in rfc2253 if escaped hex chars should
// be decoded inside quotes. Let's just forward the verbatim
// for now
value.push_back(stringv.front());
value.push_back(stringv[1]);
stringv.remove_prefix(2);
}
break;
}
case '"': {
stop = true;
stringv.remove_prefix(1);
break;
}
default: {
value.push_back(stringv.front());
stringv.remove_prefix(1);
}
}
}
if (!stop) {
// we have reached end of string, but never an actual ", so error out
return {};
}
dnPair.second = value;
} else {
std::string value;
bool stop = false;
bool lastAddedEscapedSpace = false;
while (!stringv.empty() && !stop) {
switch (stringv.front()) {
case '\\': //_escaping
{
stringv.remove_prefix(1);
if (stringv.empty()) {
return {};
}
switch (stringv.front()) {
case ',':
case '=':
case '+':
case '<':
case '>':
case '#':
case ';':
case '\\':
case '"':
case ' ': {
if (stringv.front() == ' ') {
lastAddedEscapedSpace = true;
} else {
lastAddedEscapedSpace = false;
}
value.push_back(stringv.front());
stringv.remove_prefix(1);
break;
}
default: {
if (stringv.size() < 2) {
// this should be double hex-ish, but isn't.
return {};
}
if (std::isxdigit(stringv.front()) && std::isxdigit(stringv[1])) {
lastAddedEscapedSpace = false;
value.push_back(xtoi(stringv.front(), stringv[1]));
stringv.remove_prefix(2);
break;
} else {
// invalid escape
return {};
}
}
}
break;
}
case '"':
// unescaped " in the middle; not allowed
return {};
case ',':
case '=':
case '+':
case '<':
case '>':
case '#':
case ';': {
stop = true;
break; //
}
default:
lastAddedEscapedSpace = false;
value.push_back(stringv.front());
stringv.remove_prefix(1);
}
}
if (lastAddedEscapedSpace) {
dnPair.second = value;
} else {
dnPair.second = std::string { removeTrailingSpaces(value) };
}
}
return { stringv, dnPair };
}
}
using Result = std::vector<std::pair<std::string, std::string>>;
/* Parse a DN and return an array-ized one. This is not a validating
parser and it does not support any old-stylish syntax; gpgme is
expected to return only rfc2253 compatible strings. */
static Result parseString(std::string_view string)
{
Result result;
while (!string.empty()) {
string = detail::removeLeadingSpaces(string);
if (string.empty()) {
break;
}
auto [partResult, dnPair] = detail::parse_dn_part(string);
if (!partResult.has_value()) {
return {};
}
string = partResult.value();
if (dnPair.first.size() && dnPair.second.size()) {
result.emplace_back(std::move(dnPair));
}
string = detail::removeLeadingSpaces(string);
if (string.empty()) {
break;
}
switch (string.front()) {
case ',':
case ';':
case '+':
string.remove_prefix(1);
break;
default:
// some unexpected characters here
return {};
}
}
return result;
}
/// returns the first value of a given key (note. there can be multiple)
/// or nullopt if key is not available
inline std::optional<std::string> FindFirstValue(const Result &dn, std::string_view key)
{
auto first = std::find_if(dn.begin(), dn.end(), [&key](const auto &it) { return it.first == key; });
if (first == dn.end()) {
return {};
}
return first->second;
}
} // namespace DN
#endif // DISTINGUISHEDNAMEPARSER_H