//======================================================================== // // 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 // // Derived from libkleopatra (KDE key management library) dn.cpp // //======================================================================== #ifndef DISTINGUISHEDNAMEPARSER_H #define DISTINGUISHEDNAMEPARSER_H #include #include #include #include #include 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 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> 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::pair> parse_dn_part(std::string_view stringv) { std::pair 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>; /* 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 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