summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSune Vuorela <sune@vuorela.dk>2023-03-10 13:00:09 +0100
committerSune Vuorela <sune@vuorela.dk>2023-05-21 15:07:41 +0200
commit395a5b7e5a8efa049a0eb3a4bbff41737aaf5fc1 (patch)
tree54850f6053eff68b21a8e10a6aca3b5932039890
parent720fcc1f1b7c2d13894ce4dfee4f68a40384d91b (diff)
Parser for Distinguished Names in certificates
Not all crypto libraries exposes the fields parsed, so we wil need our own. Derived from KDE's libkleo, but improved and has added test coverage
-rw-r--r--poppler/DistinguishedNameParser.h311
-rw-r--r--qt5/tests/CMakeLists.txt1
-rw-r--r--qt5/tests/check_distinguished_name_parser.cpp165
-rw-r--r--qt6/tests/CMakeLists.txt1
-rw-r--r--qt6/tests/check_distinguished_name_parser.cpp161
5 files changed, 639 insertions, 0 deletions
diff --git a/poppler/DistinguishedNameParser.h b/poppler/DistinguishedNameParser.h
new file mode 100644
index 00000000..1fef597d
--- /dev/null
+++ b/poppler/DistinguishedNameParser.h
@@ -0,0 +1,311 @@
+//========================================================================
+//
+// 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>
+
+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;
+}
+
+}
+
+#endif // DISTINGUISHEDNAMEPARSER_H
diff --git a/qt5/tests/CMakeLists.txt b/qt5/tests/CMakeLists.txt
index 3e3be3e0..297d9560 100644
--- a/qt5/tests/CMakeLists.txt
+++ b/qt5/tests/CMakeLists.txt
@@ -71,6 +71,7 @@ qt5_add_qtest(check_qt5_stroke_opacity check_stroke_opacity.cpp)
qt5_add_qtest(check_qt5_utf_conversion check_utf_conversion.cpp)
qt5_add_qtest(check_qt5_outline check_outline.cpp)
qt5_add_qtest(check_qt5_signature_basics check_signature_basics.cpp)
+qt5_add_qtest(check_qt5_distinguished_name_parser check_distinguished_name_parser.cpp)
if (NOT WIN32)
qt5_add_qtest(check_qt5_pagelabelinfo check_pagelabelinfo.cpp)
qt5_add_qtest(check_qt5_strings check_strings.cpp)
diff --git a/qt5/tests/check_distinguished_name_parser.cpp b/qt5/tests/check_distinguished_name_parser.cpp
new file mode 100644
index 00000000..48d31165
--- /dev/null
+++ b/qt5/tests/check_distinguished_name_parser.cpp
@@ -0,0 +1,165 @@
+//========================================================================
+//
+// check_distinguished_name_parser.h
+//
+// This file is licensed under the GPLv2 or later
+//
+// Copyright 2023 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk>
+//========================================================================
+#include "DistinguishedNameParser.h"
+
+#include <QtTest/QtTest>
+#include <iostream>
+
+class TestDistinguishedNameParser : public QObject
+{
+ Q_OBJECT
+public:
+ explicit TestDistinguishedNameParser(QObject *parent = nullptr) : QObject(parent) { }
+private slots:
+ // The big set of input/output. Several of the helper functions can be tested independently
+ void testParser();
+ void testParser_data();
+
+ void testRemoveLeadingSpaces();
+ void testRemoveLeadingSpaces_data();
+
+ void testRemoveTrailingSpaces();
+ void testRemoveTrailingSpaces_data();
+
+ void testParseHexString();
+ void testParseHexString_data();
+};
+
+Q_DECLARE_METATYPE(DN::Result);
+Q_DECLARE_METATYPE(std::string);
+Q_DECLARE_METATYPE(std::optional<std::string>);
+
+void TestDistinguishedNameParser::testParser()
+{
+ QFETCH(std::string, inputData);
+ QFETCH(DN::Result, expectedResult);
+
+ auto result = DN::parseString(inputData);
+ QCOMPARE(result, expectedResult);
+}
+
+void TestDistinguishedNameParser::testParser_data()
+{
+ QTest::addColumn<std::string>("inputData");
+ QTest::addColumn<DN::Result>("expectedResult");
+
+ QTest::newRow("empty") << std::string {} << DN::Result {};
+ QTest::newRow("CN=Simple") << std::string { "CN=Simple" } << DN::Result { { "CN", "Simple" } };
+ QTest::newRow("CN=Name with spaces") << std::string { "CN=Name with spaces" } << DN::Result { { "CN", "Name with spaces" } };
+ QTest::newRow("CN=Simple,O=Silly") << std::string { "CN=Simple,O=Silly" } << DN::Result { { "CN", "Simple" }, { "O", "Silly" } };
+ QTest::newRow("CN=Steve Kille,O=Isode Limited,C=GB") << std::string { "CN=Steve Kille,O=Isode Limited,C=GB" } << DN::Result { { "CN", "Steve Kille" }, { "O", "Isode Limited" }, { "C", "GB" } };
+ QTest::newRow("CN=some.user@example.com, O=MyCompany, L=San Diego,ST=California, C=US")
+ << std::string { "CN=some.user@example.com, O=MyCompany, L=San Diego,ST=California, C=US" } << DN::Result { { "CN", "some.user@example.com" }, { "O", "MyCompany" }, { "L", "San Diego" }, { "ST", "California" }, { "C", "US" } };
+ QTest::newRow("Multi valued") << std::string { "OU=Sales+CN=J. Smith,O=Widget Inc.,C=US" }
+ << DN::Result { { "OU", "Sales" }, { "CN", "J. Smith" }, { "O", "Widget Inc." }, { "C", "US" } }; // This is technically wrong, but probably good enough for now
+ QTest::newRow("Escaping comma") << std::string { "CN=L. Eagle,O=Sue\\, Grabbit and Runn,C=GB" } << DN::Result { { "CN", "L. Eagle" }, { "O", "Sue, Grabbit and Runn" }, { "C", "GB" } };
+ QTest::newRow("Escaped trailing space") << std::string { "CN=Trailing space\\ " } << DN::Result { { "CN", "Trailing space " } };
+ QTest::newRow("Escaped quote") << std::string { "CN=Quotation \\\" Mark" } << DN::Result { { "CN", "Quotation \" Mark" } };
+
+ QTest::newRow("CN=Simple with escaping") << std::string { "CN=S\\69mpl\\65\\7A" } << DN::Result { { "CN", "Simplez" } };
+ QTest::newRow("SN=Lu\\C4\\8Di\\C4\\87") << std::string { "SN=Lu\\C4\\8Di\\C4\\87" } << DN::Result { { "SN", "Lučić" } };
+ QTest::newRow("CN=\"Quoted name\"") << std::string { "CN=\"Quoted name\"" } << DN::Result { { "CN", "Quoted name" } };
+ QTest::newRow("CN=\" Leading and trailing spacees \"") << std::string { "CN=\" Leading and trailing spaces \"" } << DN::Result { { "CN", " Leading and trailing spaces " } };
+ QTest::newRow("Comma in quotes") << std::string { "CN=\"Comma, inside\"" } << DN::Result { { "CN", "Comma, inside" } };
+ QTest::newRow("forbidden chars in quotes") << std::string { "CN=\"Forbidden !@#$%&*()<>[]{},.?/\\| chars\"" } << DN::Result { { "CN", "Forbidden !@#$%&*()<>[]{},.?/\\| chars" } };
+ QTest::newRow("Quoted quotation") << std::string { "CN=\"Quotation \\\" Mark\"" } << DN::Result { { "CN", "Quotation \" Mark" } };
+ QTest::newRow("Quoted quotation") << std::string { "CN=\"Quotation \\\" Mark\\\" Multiples\"" } << DN::Result { { "CN", "Quotation \" Mark\" Multiples" } };
+
+ QTest::newRow("frompdf1") << std::string { "2.5.4.97=#5553742D49644E722E20444520313233343735323233,CN=TeleSec PKS eIDAS QES CA 5,O=Deutsche Telekom AG,C=DE" }
+ << DN::Result { { "2.5.4.97", "USt-IdNr. DE 123475223" }, { "CN", "TeleSec PKS eIDAS QES CA 5" }, { "O", "Deutsche Telekom AG" }, { "C", "DE" } };
+ QTest::newRow("frompdf2") << std::string { "2.5.4.5=#34,CN=Koch\\, Werner,2.5.4.42=#5765726E6572,2.5.4.4=#4B6F6368,C=DE" }
+ << DN::Result { { "SerialNumber", "4" }, { "CN", "Koch, Werner" }, { "GN", "Werner" }, { "SN", "Koch" }, { "C", "DE" } };
+ QTest::newRow("frompdf2a") << std::string { "2.5.4.5=#34,CN=Koch\\, Werner,oid.2.5.4.42=#5765726E6572,OID.2.5.4.4=#4B6F6368,C=DE" }
+ << DN::Result { { "SerialNumber", "4" }, { "CN", "Koch, Werner" }, { "GN", "Werner" }, { "SN", "Koch" }, { "C", "DE" } };
+
+ // weird spacing
+ QTest::newRow("CN =Simple") << std::string { "CN =Simple" } << DN::Result { { "CN", "Simple" } };
+ QTest::newRow("CN= Simple") << std::string { "CN= Simple" } << DN::Result { { "CN", "Simple" } };
+ QTest::newRow("CN=Simple ") << std::string { "CN=Simple " } << DN::Result { { "CN", "Simple" } };
+ QTest::newRow("CN=Simple,") << std::string { "CN=Simple," } << DN::Result { { "CN", "Simple" } };
+ QTest::newRow("CN=Simple, O=Silly") << std::string { "CN=Simple, O=Silly" } << DN::Result { { "CN", "Simple" }, { "O", "Silly" } };
+
+ // various malformed
+ QTest::newRow("CN=Simple\\") << std::string { "CN=Simple\\" } << DN::Result {};
+ QTest::newRow("CN=") << std::string { "CN=" } << DN::Result {};
+ QTest::newRow("CN=Simple\\X") << std::string { "CN=Simple\\X" } << DN::Result {};
+ QTest::newRow("CN=Simple, O") << std::string { "CN=Simple, O" } << DN::Result {};
+ QTest::newRow("CN=Sim\"ple") << std::string { "CN=Sim\"ple, O" } << DN::Result {};
+ QTest::newRow("CN=Simple\\a") << std::string { "CN=Simple\\a" } << DN::Result {};
+ QTest::newRow("=Simple") << std::string { "=Simple" } << DN::Result {};
+ QTest::newRow("CN=\"Simple") << std::string { "CN=\"Simple" } << DN::Result {};
+ QTest::newRow("CN=\"Simple") << std::string { "CN=\"Simple\\" } << DN::Result {};
+ QTest::newRow("unquoted quotation in quotation") << std::string { "CN=\"Quotation \" Mark\"" } << DN::Result {};
+}
+
+void TestDistinguishedNameParser::testRemoveLeadingSpaces()
+{
+ QFETCH(std::string, input);
+ QFETCH(std::string, expectedOutput);
+
+ auto result = DN::detail::removeLeadingSpaces(input);
+ QCOMPARE(result, expectedOutput);
+}
+void TestDistinguishedNameParser::testRemoveLeadingSpaces_data()
+{
+ QTest::addColumn<std::string>("input");
+ QTest::addColumn<std::string>("expectedOutput");
+
+ QTest::newRow("Empty") << std::string {} << std::string {};
+ QTest::newRow("No leading spaces") << std::string { "horse" } << std::string { "horse" };
+ QTest::newRow("Some spaces") << std::string { " horse" } << std::string { "horse" };
+ QTest::newRow("Some leading and trailing") << std::string { " horse " } << std::string { "horse " };
+}
+
+void TestDistinguishedNameParser::testRemoveTrailingSpaces()
+{
+ QFETCH(std::string, input);
+ QFETCH(std::string, expectedOutput);
+
+ auto result = DN::detail::removeTrailingSpaces(input);
+ QCOMPARE(result, expectedOutput);
+}
+void TestDistinguishedNameParser::testRemoveTrailingSpaces_data()
+{
+ QTest::addColumn<std::string>("input");
+ QTest::addColumn<std::string>("expectedOutput");
+
+ QTest::newRow("Empty") << std::string {} << std::string {};
+ QTest::newRow("No leading spaces") << std::string { "horse" } << std::string { "horse" };
+ QTest::newRow("Some spaces") << std::string { "horse " } << std::string { "horse" };
+ QTest::newRow("Some leading and trailing") << std::string { " horse " } << std::string { " horse" };
+}
+
+void TestDistinguishedNameParser::testParseHexString()
+{
+ QFETCH(std::string, input);
+ QFETCH(std::optional<std::string>, expectedOutput);
+
+ auto result = DN::detail::parseHexString(input);
+ QCOMPARE(result, expectedOutput);
+}
+
+void TestDistinguishedNameParser::testParseHexString_data()
+{
+ QTest::addColumn<std::string>("input");
+ QTest::addColumn<std::optional<std::string>>("expectedOutput");
+
+ QTest::newRow("4") << std::string { "34" } << std::optional<std::string>("4");
+ QTest::newRow("Koch") << std::string { "4B6F6368" } << std::optional<std::string>("Koch");
+ QTest::newRow("USt-IdNr. DE 123475223") << std::string { "5553742D49644E722E20444520313233343735323233" } << std::optional<std::string>("USt-IdNr. DE 123475223");
+
+ // various baddies
+ QTest::newRow("empty") << std::string {} << std::optional<std::string> {};
+ QTest::newRow("FFF") << std::string { "FFF" } << std::optional<std::string> {};
+ QTest::newRow("F") << std::string { "F" } << std::optional<std::string> {};
+ QTest::newRow("XX") << std::string { "XX" } << std::optional<std::string> {};
+}
+
+QTEST_GUILESS_MAIN(TestDistinguishedNameParser);
+#include "check_distinguished_name_parser.moc"
diff --git a/qt6/tests/CMakeLists.txt b/qt6/tests/CMakeLists.txt
index 36da5b9f..3bb09d9f 100644
--- a/qt6/tests/CMakeLists.txt
+++ b/qt6/tests/CMakeLists.txt
@@ -63,6 +63,7 @@ qt6_add_qtest(check_qt6_stroke_opacity check_stroke_opacity.cpp)
qt6_add_qtest(check_qt6_utf_conversion check_utf_conversion.cpp)
qt6_add_qtest(check_qt6_outline check_outline.cpp)
qt6_add_qtest(check_qt6_signature_basics check_signature_basics.cpp)
+qt6_add_qtest(check_qt6_distinguished_name_parser check_distinguished_name_parser.cpp)
if (NOT WIN32)
qt6_add_qtest(check_qt6_pagelabelinfo check_pagelabelinfo.cpp)
qt6_add_qtest(check_qt6_strings check_strings.cpp)
diff --git a/qt6/tests/check_distinguished_name_parser.cpp b/qt6/tests/check_distinguished_name_parser.cpp
new file mode 100644
index 00000000..95d84e98
--- /dev/null
+++ b/qt6/tests/check_distinguished_name_parser.cpp
@@ -0,0 +1,161 @@
+//========================================================================
+//
+// check_distinguished_name_parser.h
+//
+// This file is licensed under the GPLv2 or later
+//
+// Copyright 2023 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk>
+//========================================================================
+#include "DistinguishedNameParser.h"
+
+#include <QtTest/QtTest>
+#include <iostream>
+
+class TestDistinguishedNameParser : public QObject
+{
+ Q_OBJECT
+public:
+ explicit TestDistinguishedNameParser(QObject *parent = nullptr) : QObject(parent) { }
+private slots:
+ // The big set of input/output. Several of the helper functions can be tested independently
+ void testParser();
+ void testParser_data();
+
+ void testRemoveLeadingSpaces();
+ void testRemoveLeadingSpaces_data();
+
+ void testRemoveTrailingSpaces();
+ void testRemoveTrailingSpaces_data();
+
+ void testParseHexString();
+ void testParseHexString_data();
+};
+
+void TestDistinguishedNameParser::testParser()
+{
+ QFETCH(std::string, inputData);
+ QFETCH(DN::Result, expectedResult);
+
+ auto result = DN::parseString(inputData);
+ QCOMPARE(result, expectedResult);
+}
+
+void TestDistinguishedNameParser::testParser_data()
+{
+ QTest::addColumn<std::string>("inputData");
+ QTest::addColumn<DN::Result>("expectedResult");
+
+ QTest::newRow("empty") << std::string {} << DN::Result {};
+ QTest::newRow("CN=Simple") << std::string { "CN=Simple" } << DN::Result { { "CN", "Simple" } };
+ QTest::newRow("CN=Name with spaces") << std::string { "CN=Name with spaces" } << DN::Result { { "CN", "Name with spaces" } };
+ QTest::newRow("CN=Simple,O=Silly") << std::string { "CN=Simple,O=Silly" } << DN::Result { { "CN", "Simple" }, { "O", "Silly" } };
+ QTest::newRow("CN=Steve Kille,O=Isode Limited,C=GB") << std::string { "CN=Steve Kille,O=Isode Limited,C=GB" } << DN::Result { { "CN", "Steve Kille" }, { "O", "Isode Limited" }, { "C", "GB" } };
+ QTest::newRow("CN=some.user@example.com, O=MyCompany, L=San Diego,ST=California, C=US")
+ << std::string { "CN=some.user@example.com, O=MyCompany, L=San Diego,ST=California, C=US" } << DN::Result { { "CN", "some.user@example.com" }, { "O", "MyCompany" }, { "L", "San Diego" }, { "ST", "California" }, { "C", "US" } };
+ QTest::newRow("Multi valued") << std::string { "OU=Sales+CN=J. Smith,O=Widget Inc.,C=US" }
+ << DN::Result { { "OU", "Sales" }, { "CN", "J. Smith" }, { "O", "Widget Inc." }, { "C", "US" } }; // This is technically wrong, but probably good enough for now
+ QTest::newRow("Escaping comma") << std::string { "CN=L. Eagle,O=Sue\\, Grabbit and Runn,C=GB" } << DN::Result { { "CN", "L. Eagle" }, { "O", "Sue, Grabbit and Runn" }, { "C", "GB" } };
+ QTest::newRow("Escaped trailing space") << std::string { "CN=Trailing space\\ " } << DN::Result { { "CN", "Trailing space " } };
+ QTest::newRow("Escaped quote") << std::string { "CN=Quotation \\\" Mark" } << DN::Result { { "CN", "Quotation \" Mark" } };
+
+ QTest::newRow("CN=Simple with escaping") << std::string { "CN=S\\69mpl\\65\\7A" } << DN::Result { { "CN", "Simplez" } };
+ QTest::newRow("SN=Lu\\C4\\8Di\\C4\\87") << std::string { "SN=Lu\\C4\\8Di\\C4\\87" } << DN::Result { { "SN", "Lučić" } };
+ QTest::newRow("CN=\"Quoted name\"") << std::string { "CN=\"Quoted name\"" } << DN::Result { { "CN", "Quoted name" } };
+ QTest::newRow("CN=\" Leading and trailing spacees \"") << std::string { "CN=\" Leading and trailing spaces \"" } << DN::Result { { "CN", " Leading and trailing spaces " } };
+ QTest::newRow("Comma in quotes") << std::string { "CN=\"Comma, inside\"" } << DN::Result { { "CN", "Comma, inside" } };
+ QTest::newRow("forbidden chars in quotes") << std::string { "CN=\"Forbidden !@#$%&*()<>[]{},.?/\\| chars\"" } << DN::Result { { "CN", "Forbidden !@#$%&*()<>[]{},.?/\\| chars" } };
+ QTest::newRow("Quoted quotation") << std::string { "CN=\"Quotation \\\" Mark\"" } << DN::Result { { "CN", "Quotation \" Mark" } };
+ QTest::newRow("Quoted quotation") << std::string { "CN=\"Quotation \\\" Mark\\\" Multiples\"" } << DN::Result { { "CN", "Quotation \" Mark\" Multiples" } };
+
+ QTest::newRow("frompdf1") << std::string { "2.5.4.97=#5553742D49644E722E20444520313233343735323233,CN=TeleSec PKS eIDAS QES CA 5,O=Deutsche Telekom AG,C=DE" }
+ << DN::Result { { "2.5.4.97", "USt-IdNr. DE 123475223" }, { "CN", "TeleSec PKS eIDAS QES CA 5" }, { "O", "Deutsche Telekom AG" }, { "C", "DE" } };
+ QTest::newRow("frompdf2") << std::string { "2.5.4.5=#34,CN=Koch\\, Werner,2.5.4.42=#5765726E6572,2.5.4.4=#4B6F6368,C=DE" }
+ << DN::Result { { "SerialNumber", "4" }, { "CN", "Koch, Werner" }, { "GN", "Werner" }, { "SN", "Koch" }, { "C", "DE" } };
+ QTest::newRow("frompdf2a") << std::string { "2.5.4.5=#34,CN=Koch\\, Werner,oid.2.5.4.42=#5765726E6572,OID.2.5.4.4=#4B6F6368,C=DE" }
+ << DN::Result { { "SerialNumber", "4" }, { "CN", "Koch, Werner" }, { "GN", "Werner" }, { "SN", "Koch" }, { "C", "DE" } };
+
+ // weird spacing
+ QTest::newRow("CN =Simple") << std::string { "CN =Simple" } << DN::Result { { "CN", "Simple" } };
+ QTest::newRow("CN= Simple") << std::string { "CN= Simple" } << DN::Result { { "CN", "Simple" } };
+ QTest::newRow("CN=Simple ") << std::string { "CN=Simple " } << DN::Result { { "CN", "Simple" } };
+ QTest::newRow("CN=Simple,") << std::string { "CN=Simple," } << DN::Result { { "CN", "Simple" } };
+ QTest::newRow("CN=Simple, O=Silly") << std::string { "CN=Simple, O=Silly" } << DN::Result { { "CN", "Simple" }, { "O", "Silly" } };
+
+ // various malformed
+ QTest::newRow("CN=Simple\\") << std::string { "CN=Simple\\" } << DN::Result {};
+ QTest::newRow("CN=") << std::string { "CN=" } << DN::Result {};
+ QTest::newRow("CN=Simple\\X") << std::string { "CN=Simple\\X" } << DN::Result {};
+ QTest::newRow("CN=Simple, O") << std::string { "CN=Simple, O" } << DN::Result {};
+ QTest::newRow("CN=Sim\"ple") << std::string { "CN=Sim\"ple, O" } << DN::Result {};
+ QTest::newRow("CN=Simple\\a") << std::string { "CN=Simple\\a" } << DN::Result {};
+ QTest::newRow("=Simple") << std::string { "=Simple" } << DN::Result {};
+ QTest::newRow("CN=\"Simple") << std::string { "CN=\"Simple" } << DN::Result {};
+ QTest::newRow("CN=\"Simple") << std::string { "CN=\"Simple\\" } << DN::Result {};
+ QTest::newRow("unquoted quotation in quotation") << std::string { "CN=\"Quotation \" Mark\"" } << DN::Result {};
+}
+
+void TestDistinguishedNameParser::testRemoveLeadingSpaces()
+{
+ QFETCH(std::string, input);
+ QFETCH(std::string, expectedOutput);
+
+ auto result = DN::detail::removeLeadingSpaces(input);
+ QCOMPARE(result, expectedOutput);
+}
+void TestDistinguishedNameParser::testRemoveLeadingSpaces_data()
+{
+ QTest::addColumn<std::string>("input");
+ QTest::addColumn<std::string>("expectedOutput");
+
+ QTest::newRow("Empty") << std::string {} << std::string {};
+ QTest::newRow("No leading spaces") << std::string { "horse" } << std::string { "horse" };
+ QTest::newRow("Some spaces") << std::string { " horse" } << std::string { "horse" };
+ QTest::newRow("Some leading and trailing") << std::string { " horse " } << std::string { "horse " };
+}
+
+void TestDistinguishedNameParser::testRemoveTrailingSpaces()
+{
+ QFETCH(std::string, input);
+ QFETCH(std::string, expectedOutput);
+
+ auto result = DN::detail::removeTrailingSpaces(input);
+ QCOMPARE(result, expectedOutput);
+}
+void TestDistinguishedNameParser::testRemoveTrailingSpaces_data()
+{
+ QTest::addColumn<std::string>("input");
+ QTest::addColumn<std::string>("expectedOutput");
+
+ QTest::newRow("Empty") << std::string {} << std::string {};
+ QTest::newRow("No leading spaces") << std::string { "horse" } << std::string { "horse" };
+ QTest::newRow("Some spaces") << std::string { "horse " } << std::string { "horse" };
+ QTest::newRow("Some leading and trailing") << std::string { " horse " } << std::string { " horse" };
+}
+
+void TestDistinguishedNameParser::testParseHexString()
+{
+ QFETCH(std::string, input);
+ QFETCH(std::optional<std::string>, expectedOutput);
+
+ auto result = DN::detail::parseHexString(input);
+ QCOMPARE(result, expectedOutput);
+}
+
+void TestDistinguishedNameParser::testParseHexString_data()
+{
+ QTest::addColumn<std::string>("input");
+ QTest::addColumn<std::optional<std::string>>("expectedOutput");
+
+ QTest::newRow("4") << std::string { "34" } << std::optional<std::string>("4");
+ QTest::newRow("Koch") << std::string { "4B6F6368" } << std::optional<std::string>("Koch");
+ QTest::newRow("USt-IdNr. DE 123475223") << std::string { "5553742D49644E722E20444520313233343735323233" } << std::optional<std::string>("USt-IdNr. DE 123475223");
+
+ // various baddies
+ QTest::newRow("empty") << std::string {} << std::optional<std::string> {};
+ QTest::newRow("FFF") << std::string { "FFF" } << std::optional<std::string> {};
+ QTest::newRow("F") << std::string { "F" } << std::optional<std::string> {};
+ QTest::newRow("XX") << std::string { "XX" } << std::optional<std::string> {};
+}
+
+QTEST_GUILESS_MAIN(TestDistinguishedNameParser);
+#include "check_distinguished_name_parser.moc"