1 | //======================================================================== |
2 | // |
3 | // DistinguishedNameParser.h |
4 | // |
5 | // This file is licensed under the GPLv2 or later |
6 | // |
7 | // Copyright 2002 g10 Code GmbH |
8 | // Copyright 2004 Klarälvdalens Datakonsult AB |
9 | // Copyright 2021 g10 Code GmbH |
10 | // Copyright 2023 g10 Code GmbH, Author: Sune Stolborg Vuorela <sune@vuorela.dk> |
11 | // |
12 | // Derived from libkleopatra (KDE key management library) dn.cpp |
13 | // |
14 | //======================================================================== |
15 | |
16 | #ifndef DISTINGUISHEDNAMEPARSER_H |
17 | #define DISTINGUISHEDNAMEPARSER_H |
18 | |
19 | #include <vector> |
20 | #include <string> |
21 | #include <utility> |
22 | #include <optional> |
23 | #include <algorithm> |
24 | |
25 | namespace DN { |
26 | namespace detail { |
27 | |
28 | inline std::string_view removeLeadingSpaces(std::string_view view) |
29 | { |
30 | auto pos = view.find_first_not_of(c: ' '); |
31 | if (pos > view.size()) { |
32 | return {}; |
33 | } |
34 | return view.substr(pos: pos); |
35 | } |
36 | |
37 | inline std::string_view removeTrailingSpaces(std::string_view view) |
38 | { |
39 | auto pos = view.find_last_not_of(c: ' '); |
40 | if (pos > view.size()) { |
41 | return {}; |
42 | } |
43 | return view.substr(pos: 0, n: pos + 1); |
44 | } |
45 | |
46 | inline unsigned char xtoi(unsigned char c) |
47 | { |
48 | if (c <= '9') { |
49 | return c - '0'; |
50 | } |
51 | if (c <= 'F') { |
52 | return c - 'A' + 10; |
53 | } |
54 | return c < 'a' + 10; |
55 | } |
56 | |
57 | inline unsigned char xtoi(unsigned char first, unsigned char second) |
58 | { |
59 | return 16 * xtoi(c: first) + xtoi(c: second); |
60 | } |
61 | // Parses a hex string into actual content |
62 | inline std::optional<std::string> parseHexString(std::string_view view) |
63 | { |
64 | auto size = view.size(); |
65 | if (size == 0 || (size % 2 == 1)) { |
66 | return std::nullopt; |
67 | } |
68 | // It is only supposed to be called with actual hex strings |
69 | // but this is just to be extra sure |
70 | auto endHex = view.find_first_not_of(str: "1234567890abcdefABCDEF" ); |
71 | if (endHex != std::string_view::npos) { |
72 | return {}; |
73 | } |
74 | std::string result; |
75 | result.reserve(res: size / 2); |
76 | for (size_t i = 0; i < (view.size() - 1); i += 2) { |
77 | result.push_back(c: xtoi(first: view[i], second: view[i + 1])); |
78 | } |
79 | return result; |
80 | } |
81 | |
82 | static const std::vector<std::pair<std::string_view, std::string_view>> oidmap = { |
83 | // clang-format off |
84 | // keep them ordered by oid: |
85 | {"NameDistinguisher" , "0.2.262.1.10.7.20" }, |
86 | {"EMAIL" , "1.2.840.113549.1.9.1" }, |
87 | {"CN" , "2.5.4.3" }, |
88 | {"SN" , "2.5.4.4" }, |
89 | {"SerialNumber" , "2.5.4.5" }, |
90 | {"T" , "2.5.4.12" }, |
91 | {"D" , "2.5.4.13" }, |
92 | {"BC" , "2.5.4.15" }, |
93 | {"ADDR" , "2.5.4.16" }, |
94 | {"PC" , "2.5.4.17" }, |
95 | {"GN" , "2.5.4.42" }, |
96 | {"Pseudo" , "2.5.4.65" }, |
97 | // clang-format on |
98 | }; |
99 | |
100 | static std::string_view attributeNameForOID(std::string_view oid) |
101 | { |
102 | if (oid.substr(pos: 0, n: 4) == std::string_view { "OID." } || oid.substr(pos: 0, n: 4) == std::string_view { "oid." }) { // c++20 has starts_with. we don't have that yet. |
103 | oid.remove_prefix(n: 4); |
104 | } |
105 | for (const auto &m : oidmap) { |
106 | if (oid == m.second) { |
107 | return m.first; |
108 | } |
109 | } |
110 | return {}; |
111 | } |
112 | |
113 | /* Parse a DN and return an array-ized one. This is not a validating |
114 | parser and it does not support any old-stylish syntax; gpgme is |
115 | expected to return only rfc2253 compatible strings. */ |
116 | static std::pair<std::optional<std::string_view>, std::pair<std::string, std::string>> parse_dn_part(std::string_view stringv) |
117 | { |
118 | std::pair<std::string, std::string> dnPair; |
119 | auto separatorPos = stringv.find_first_of(c: '='); |
120 | if (separatorPos == 0 || separatorPos == std::string_view::npos) { |
121 | return {}; /* empty key */ |
122 | } |
123 | |
124 | std::string_view key = stringv.substr(pos: 0, n: separatorPos); |
125 | key = removeTrailingSpaces(view: key); |
126 | // map OIDs to their names: |
127 | if (auto name = attributeNameForOID(oid: key); !name.empty()) { |
128 | key = name; |
129 | } |
130 | |
131 | dnPair.first = std::string { key }; |
132 | stringv = removeLeadingSpaces(view: stringv.substr(pos: separatorPos + 1)); |
133 | if (stringv.empty()) { |
134 | return {}; |
135 | } |
136 | |
137 | if (stringv.front() == '#') { |
138 | /* hexstring */ |
139 | stringv.remove_prefix(n: 1); |
140 | auto endHex = stringv.find_first_not_of(str: "1234567890abcdefABCDEF" ); |
141 | if (!endHex || (endHex % 2 == 1)) { |
142 | return {}; /* empty or odd number of digits */ |
143 | } |
144 | auto value = parseHexString(view: stringv.substr(pos: 0, n: endHex)); |
145 | if (!value.has_value()) { |
146 | return {}; |
147 | } |
148 | stringv = stringv.substr(pos: endHex); |
149 | dnPair.second = value.value(); |
150 | } else if (stringv.front() == '"') { |
151 | stringv.remove_prefix(n: 1); |
152 | std::string value; |
153 | bool stop = false; |
154 | while (!stringv.empty() && !stop) { |
155 | switch (stringv.front()) { |
156 | case '\\': { |
157 | if (stringv.size() < 2) { |
158 | return {}; |
159 | } |
160 | if (stringv[1] == '"') { |
161 | value.push_back(c: '"'); |
162 | stringv.remove_prefix(n: 2); |
163 | } else { |
164 | // it is a bit unclear in rfc2253 if escaped hex chars should |
165 | // be decoded inside quotes. Let's just forward the verbatim |
166 | // for now |
167 | value.push_back(c: stringv.front()); |
168 | value.push_back(c: stringv[1]); |
169 | stringv.remove_prefix(n: 2); |
170 | } |
171 | break; |
172 | } |
173 | case '"': { |
174 | stop = true; |
175 | stringv.remove_prefix(n: 1); |
176 | break; |
177 | } |
178 | default: { |
179 | value.push_back(c: stringv.front()); |
180 | stringv.remove_prefix(n: 1); |
181 | } |
182 | } |
183 | } |
184 | if (!stop) { |
185 | // we have reached end of string, but never an actual ", so error out |
186 | return {}; |
187 | } |
188 | dnPair.second = value; |
189 | } else { |
190 | std::string value; |
191 | bool stop = false; |
192 | bool lastAddedEscapedSpace = false; |
193 | while (!stringv.empty() && !stop) { |
194 | switch (stringv.front()) { |
195 | case '\\': //_escaping |
196 | { |
197 | stringv.remove_prefix(n: 1); |
198 | if (stringv.empty()) { |
199 | return {}; |
200 | } |
201 | switch (stringv.front()) { |
202 | case ',': |
203 | case '=': |
204 | case '+': |
205 | case '<': |
206 | case '>': |
207 | case '#': |
208 | case ';': |
209 | case '\\': |
210 | case '"': |
211 | case ' ': { |
212 | if (stringv.front() == ' ') { |
213 | lastAddedEscapedSpace = true; |
214 | } else { |
215 | lastAddedEscapedSpace = false; |
216 | } |
217 | value.push_back(c: stringv.front()); |
218 | stringv.remove_prefix(n: 1); |
219 | break; |
220 | } |
221 | default: { |
222 | if (stringv.size() < 2) { |
223 | // this should be double hex-ish, but isn't. |
224 | return {}; |
225 | } |
226 | if (std::isxdigit(stringv.front()) && std::isxdigit(stringv[1])) { |
227 | lastAddedEscapedSpace = false; |
228 | value.push_back(c: xtoi(first: stringv.front(), second: stringv[1])); |
229 | stringv.remove_prefix(n: 2); |
230 | break; |
231 | } else { |
232 | // invalid escape |
233 | return {}; |
234 | } |
235 | } |
236 | } |
237 | break; |
238 | } |
239 | case '"': |
240 | // unescaped " in the middle; not allowed |
241 | return {}; |
242 | case ',': |
243 | case '=': |
244 | case '+': |
245 | case '<': |
246 | case '>': |
247 | case '#': |
248 | case ';': { |
249 | stop = true; |
250 | break; // |
251 | } |
252 | default: |
253 | lastAddedEscapedSpace = false; |
254 | value.push_back(c: stringv.front()); |
255 | stringv.remove_prefix(n: 1); |
256 | } |
257 | } |
258 | if (lastAddedEscapedSpace) { |
259 | dnPair.second = value; |
260 | } else { |
261 | dnPair.second = std::string { removeTrailingSpaces(view: value) }; |
262 | } |
263 | } |
264 | return { stringv, dnPair }; |
265 | } |
266 | } |
267 | |
268 | using Result = std::vector<std::pair<std::string, std::string>>; |
269 | |
270 | /* Parse a DN and return an array-ized one. This is not a validating |
271 | parser and it does not support any old-stylish syntax; gpgme is |
272 | expected to return only rfc2253 compatible strings. */ |
273 | static Result parseString(std::string_view string) |
274 | { |
275 | Result result; |
276 | while (!string.empty()) { |
277 | string = detail::removeLeadingSpaces(view: string); |
278 | if (string.empty()) { |
279 | break; |
280 | } |
281 | |
282 | auto [partResult, dnPair] = detail::parse_dn_part(stringv: string); |
283 | if (!partResult.has_value()) { |
284 | return {}; |
285 | } |
286 | |
287 | string = partResult.value(); |
288 | if (dnPair.first.size() && dnPair.second.size()) { |
289 | result.emplace_back(args: std::move(dnPair)); |
290 | } |
291 | |
292 | string = detail::removeLeadingSpaces(view: string); |
293 | if (string.empty()) { |
294 | break; |
295 | } |
296 | switch (string.front()) { |
297 | case ',': |
298 | case ';': |
299 | case '+': |
300 | string.remove_prefix(n: 1); |
301 | break; |
302 | default: |
303 | // some unexpected characters here |
304 | return {}; |
305 | } |
306 | } |
307 | return result; |
308 | } |
309 | |
310 | /// returns the first value of a given key (note. there can be multiple) |
311 | /// or nullopt if key is not available |
312 | inline std::optional<std::string> FindFirstValue(const Result &dn, std::string_view key) |
313 | { |
314 | auto first = std::find_if(first: dn.begin(), last: dn.end(), pred: [&key](const auto &it) { return it.first == key; }); |
315 | if (first == dn.end()) { |
316 | return {}; |
317 | } |
318 | return first->second; |
319 | } |
320 | } // namespace DN |
321 | #endif // DISTINGUISHEDNAMEPARSER_H |
322 | |