Initial commit.
[BoarSSL] / X500 / DNPart.cs
1 /*
2 * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
3 *
4 * Permission is hereby granted, free of charge, to any person obtaining
5 * a copy of this software and associated documentation files (the
6 * "Software"), to deal in the Software without restriction, including
7 * without limitation the rights to use, copy, modify, merge, publish,
8 * distribute, sublicense, and/or sell copies of the Software, and to
9 * permit persons to whom the Software is furnished to do so, subject to
10 * the following conditions:
11 *
12 * The above copyright notice and this permission notice shall be
13 * included in all copies or substantial portions of the Software.
14 *
15 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
19 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
20 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
21 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 * SOFTWARE.
23 */
24
25 using System;
26 using System.Collections.Generic;
27 using System.IO;
28 using System.Text;
29
30 using Asn1;
31
32 namespace X500 {
33
34 /*
35 * A DNPart instance encodes an X.500 name element: it has a type (an
36 * OID) and a value. The value is an ASN.1 object. If the name type is
37 * one of a list of standard types, then the value is a character
38 * string, and there is a "friendly type" which is a character string
39 * (such as "CN" for the "common name", of OID 2.5.4.3).
40 */
41
42 public class DNPart {
43
44 /*
45 * These are the known "friendly types". The values decode as
46 * strings.
47 */
48
49 public const string COMMON_NAME = "CN";
50 public const string LOCALITY = "L";
51 public const string STATE = "ST";
52 public const string ORGANIZATION = "O";
53 public const string ORGANIZATIONAL_UNIT = "OU";
54 public const string COUNTRY = "C";
55 public const string STREET = "STREET";
56 public const string DOMAIN_COMPONENT = "DC";
57 public const string USER_ID = "UID";
58 public const string EMAIL_ADDRESS = "EMAILADDRESS";
59
60 /*
61 * Get the type OID (decimal-dotted string representation).
62 */
63 public string OID {
64 get {
65 return OID_;
66 }
67 }
68 string OID_;
69
70 /*
71 * Get the string value for this element. If the element value
72 * could not be decoded as a string, then this method returns
73 * null.
74 *
75 * (Decoding error for name elements of a standard type trigger
76 * exceptions upon instance creation. Thus, a null value is
77 * possible only for a name element that uses an unknown type.)
78 */
79 public string Value {
80 get {
81 return Value_;
82 }
83 }
84 string Value_;
85
86 /*
87 * Tell whether this element is string based. This property
88 * returns true if and only if Value returns a non-null value.
89 */
90 public bool IsString {
91 get {
92 return Value_ != null;
93 }
94 }
95
96 /*
97 * Get the element value as an ASN.1 structure.
98 */
99 public AsnElt AsnValue {
100 get {
101 return AsnValue_;
102 }
103 }
104 AsnElt AsnValue_;
105
106 /*
107 * Get the "friendly type" for this element. This is the
108 * string mnemonic such as "CN" for "common name". If no
109 * friendly type is known for that element, then the OID
110 * is returned (decimal-dotted representation).
111 */
112 public string FriendlyType {
113 get {
114 return GetFriendlyType(OID);
115 }
116 }
117
118 /*
119 * "Normalized" string value (converted to uppercase then
120 * lowercase, leading and trailing whitespace trimmed, adjacent
121 * spaces coalesced). This should allow for efficient comparison
122 * while still supporting most corner cases.
123 *
124 * This does not implement full RFC 4518 rules, but it should
125 * be good enough for an analysis tool.
126 */
127 string normValue;
128
129 byte[] encodedValue;
130 int hashCode;
131
132 internal DNPart(string oid, AsnElt val)
133 {
134 OID_ = oid;
135 AsnValue_ = val;
136 encodedValue = val.Encode();
137 uint hc = (uint)oid.GetHashCode();
138 try {
139 string s = val.GetString();
140 Value_ = s;
141 s = s.ToUpperInvariant().ToLowerInvariant();
142 StringBuilder sb = new StringBuilder();
143 bool lwws = true;
144 foreach (char c in s.Trim()) {
145 if (IsControl(c)) {
146 continue;
147 }
148 if (IsWS(c)) {
149 if (lwws) {
150 continue;
151 }
152 lwws = true;
153 sb.Append(' ');
154 } else {
155 sb.Append(c);
156 }
157 }
158 int n = sb.Length;
159 if (n > 0 && sb[n - 1] == ' ') {
160 sb.Length = n - 1;
161 }
162 normValue = sb.ToString();
163 hc += (uint)normValue.GetHashCode();
164 } catch {
165 if (OID_TO_FT.ContainsKey(oid)) {
166 throw;
167 }
168 Value_ = null;
169 foreach (byte b in encodedValue) {
170 hc = ((hc << 7) | (hc >> 25)) ^ (uint)b;
171 }
172 }
173 hashCode = (int)hc;
174 }
175
176 static bool MustEscape(int x)
177 {
178 if (x < 0x20 || x >= 0x7F) {
179 return true;
180 }
181 switch (x) {
182 case '"':
183 case '+':
184 case ',':
185 case ';':
186 case '<':
187 case '>':
188 case '\\':
189 return true;
190 default:
191 return false;
192 }
193 }
194
195 /*
196 * Convert this element to a string. This uses RFC 4514 rules.
197 */
198 public override string ToString()
199 {
200 StringBuilder sb = new StringBuilder();
201 string ft;
202 if (OID_TO_FT.TryGetValue(OID, out ft) && IsString) {
203 sb.Append(ft);
204 sb.Append("=");
205 byte[] buf = Encoding.UTF8.GetBytes(Value);
206 for (int i = 0; i < buf.Length; i ++) {
207 byte b = buf[i];
208 if ((i == 0 && (b == ' ' || b == '#'))
209 || (i == buf.Length - 1 && b == ' ')
210 || MustEscape(b))
211 {
212 switch ((char)b) {
213 case ' ':
214 case '"':
215 case '#':
216 case '+':
217 case ',':
218 case ';':
219 case '<':
220 case '=':
221 case '>':
222 case '\\':
223 sb.Append('\\');
224 sb.Append((char)b);
225 break;
226 default:
227 sb.AppendFormat("\\{0:X2}", b);
228 break;
229 }
230 } else {
231 sb.Append((char)b);
232 }
233 }
234 } else {
235 sb.Append(OID);
236 sb.Append("=#");
237 foreach (byte b in AsnValue.Encode()) {
238 sb.AppendFormat("{0:X2}", b);
239 }
240 }
241 return sb.ToString();
242 }
243
244 /*
245 * Get the friendly type corresponding to the given OID
246 * (decimal-dotted representation). If no such type is known,
247 * then the OID string is returned.
248 */
249 public static string GetFriendlyType(string oid)
250 {
251 string ft;
252 if (OID_TO_FT.TryGetValue(oid, out ft)) {
253 return ft;
254 }
255 return oid;
256 }
257
258 static int HexVal(char c)
259 {
260 if (c >= '0' && c <= '9') {
261 return c - '0';
262 } else if (c >= 'A' && c <= 'F') {
263 return c - ('A' - 10);
264 } else if (c >= 'a' && c <= 'f') {
265 return c - ('a' - 10);
266 } else {
267 return -1;
268 }
269 }
270
271 static int HexValCheck(char c)
272 {
273 int x = HexVal(c);
274 if (x < 0) {
275 throw new AsnException(String.Format(
276 "Not an hex digit: U+{0:X4}", c));
277 }
278 return x;
279 }
280
281 static int HexVal2(string str, int k)
282 {
283 if (k >= str.Length) {
284 throw new AsnException("Missing hex digits");
285 }
286 int x = HexVal(str[k]);
287 if ((k + 1) >= str.Length) {
288 throw new AsnException("Odd number of hex digits");
289 }
290 return (x << 4) + HexVal(str[k + 1]);
291 }
292
293 static int ReadHexEscape(string str, ref int off)
294 {
295 if (off >= str.Length || str[off] != '\\') {
296 return -1;
297 }
298 if ((off + 1) >= str.Length) {
299 throw new AsnException("Truncated escape");
300 }
301 int x = HexVal(str[off + 1]);
302 if (x < 0) {
303 return -1;
304 }
305 if ((off + 2) >= str.Length) {
306 throw new AsnException("Truncated escape");
307 }
308 int y = HexValCheck(str[off + 2]);
309 off += 3;
310 return (x << 4) + y;
311 }
312
313 static int ReadHexUTF(string str, ref int off)
314 {
315 int x = ReadHexEscape(str, ref off);
316 if (x < 0x80 || x >= 0xC0) {
317 throw new AsnException(
318 "Invalid hex escape: not UTF-8");
319 }
320 return x;
321 }
322
323 static string UnEscapeUTF8(string str)
324 {
325 StringBuilder sb = new StringBuilder();
326 int n = str.Length;
327 int k = 0;
328 while (k < n) {
329 char c = str[k];
330 if (c != '\\') {
331 sb.Append(c);
332 k ++;
333 continue;
334 }
335 int x = ReadHexEscape(str, ref k);
336 if (x < 0) {
337 sb.Append(str[k + 1]);
338 k += 2;
339 continue;
340 }
341 if (x < 0x80) {
342 // nothing
343 } else if (x < 0xC0) {
344 throw new AsnException(
345 "Invalid hex escape: not UTF-8");
346 } else if (x < 0xE0) {
347 x &= 0x1F;
348 x = (x << 6) | ReadHexUTF(str, ref k) & 0x3F;
349 } else if (x < 0xF0) {
350 x &= 0x0F;
351 x = (x << 6) | ReadHexUTF(str, ref k) & 0x3F;
352 x = (x << 6) | ReadHexUTF(str, ref k) & 0x3F;
353 } else if (x < 0xF8) {
354 x &= 0x07;
355 x = (x << 6) | ReadHexUTF(str, ref k) & 0x3F;
356 x = (x << 6) | ReadHexUTF(str, ref k) & 0x3F;
357 x = (x << 6) | ReadHexUTF(str, ref k) & 0x3F;
358 if (x > 0x10FFFF) {
359 throw new AsnException("Invalid"
360 + " hex escape: out of range");
361 }
362 } else {
363 throw new AsnException(
364 "Invalid hex escape: not UTF-8");
365 }
366 if (x < 0x10000) {
367 sb.Append((char)x);
368 } else {
369 x -= 0x10000;
370 sb.Append((char)(0xD800 + (x >> 10)));
371 sb.Append((char)(0xDC00 + (x & 0x3FF)));
372 }
373 }
374 return sb.ToString();
375 }
376
377 internal static DNPart Parse(string str)
378 {
379 int j = str.IndexOf('=');
380 if (j < 0) {
381 throw new AsnException("Invalid DN: no '=' sign");
382 }
383 string a = str.Substring(0, j).Trim();
384 string b = str.Substring(j + 1).Trim();
385 string oid;
386 if (!FT_TO_OID.TryGetValue(a, out oid)) {
387 oid = AsnElt.MakeOID(oid).GetOID();
388 }
389 AsnElt aVal;
390 if (b.StartsWith("#")) {
391 MemoryStream ms = new MemoryStream();
392 int n = b.Length;
393 for (int k = 1; k < n; k += 2) {
394 int x = HexValCheck(b[k]);
395 if (k + 1 >= n) {
396 throw new AsnException(
397 "Odd number of hex digits");
398 }
399 x = (x << 4) + HexValCheck(b[k + 1]);
400 ms.WriteByte((byte)x);
401 }
402 try {
403 aVal = AsnElt.Decode(ms.ToArray());
404 } catch (Exception e) {
405 throw new AsnException("Bad DN value: "
406 + e.Message);
407 }
408 } else {
409 b = UnEscapeUTF8(b);
410 int type = AsnElt.PrintableString;
411 foreach (char c in b) {
412 if (!AsnElt.IsPrintable(c)) {
413 type = AsnElt.UTF8String;
414 break;
415 }
416 }
417 aVal = AsnElt.MakeString(type, b);
418 }
419 return new DNPart(oid, aVal);
420 }
421
422 static Dictionary<string, string> OID_TO_FT;
423 static Dictionary<string, string> FT_TO_OID;
424
425 static void AddFT(string oid, string ft)
426 {
427 OID_TO_FT[oid] = ft;
428 FT_TO_OID[ft] = oid;
429 }
430
431 static DNPart()
432 {
433 OID_TO_FT = new Dictionary<string, string>();
434 FT_TO_OID = new Dictionary<string, string>(
435 StringComparer.OrdinalIgnoreCase);
436 AddFT("2.5.4.3", COMMON_NAME);
437 AddFT("2.5.4.7", LOCALITY);
438 AddFT("2.5.4.8", STATE);
439 AddFT("2.5.4.10", ORGANIZATION);
440 AddFT("2.5.4.11", ORGANIZATIONAL_UNIT);
441 AddFT("2.5.4.6", COUNTRY);
442 AddFT("2.5.4.9", STREET);
443 AddFT("0.9.2342.19200300.100.1.25", DOMAIN_COMPONENT);
444 AddFT("0.9.2342.19200300.100.1.1", USER_ID);
445 AddFT("1.2.840.113549.1.9.1", EMAIL_ADDRESS);
446
447 /*
448 * We also accept 'S' as an alias for 'ST' because some
449 * Microsoft software uses it.
450 */
451 FT_TO_OID["S"] = FT_TO_OID["ST"];
452 }
453
454 /*
455 * Tell whether a given character is a "control character" (to
456 * be ignored for DN comparison purposes). This follows RFC 4518
457 * but only for code points in the first plane.
458 */
459 static bool IsControl(char c)
460 {
461 if (c <= 0x0008
462 || (c >= 0x000E && c <= 0x001F)
463 || (c >= 0x007F && c <= 0x0084)
464 || (c >= 0x0086 && c <= 0x009F)
465 || c == 0x06DD
466 || c == 0x070F
467 || c == 0x180E
468 || (c >= 0x200C && c <= 0x200F)
469 || (c >= 0x202A && c <= 0x202E)
470 || (c >= 0x2060 && c <= 0x2063)
471 || (c >= 0x206A && c <= 0x206F)
472 || c == 0xFEFF
473 || (c >= 0xFFF9 && c <= 0xFFFB))
474 {
475 return true;
476 }
477 return false;
478 }
479
480 /*
481 * Tell whether a character is whitespace. This follows
482 * rules of RFC 4518.
483 */
484 static bool IsWS(char c)
485 {
486 if (c == 0x0020
487 || c == 0x00A0
488 || c == 0x1680
489 || (c >= 0x2000 && c <= 0x200A)
490 || c == 0x2028
491 || c == 0x2029
492 || c == 0x202F
493 || c == 0x205F
494 || c == 0x3000)
495 {
496 return true;
497 }
498 return false;
499 }
500
501 public override bool Equals(object obj)
502 {
503 return Equals(obj as DNPart);
504 }
505
506 public bool Equals(DNPart dnp)
507 {
508 if (dnp == null) {
509 return false;
510 }
511 if (OID != dnp.OID) {
512 return false;
513 }
514 if (IsString) {
515 return dnp.IsString
516 && normValue == dnp.normValue;
517 } else if (dnp.IsString) {
518 return false;
519 } else {
520 return Eq(encodedValue, dnp.encodedValue);
521 }
522 }
523
524 public override int GetHashCode()
525 {
526 return hashCode;
527 }
528
529 static bool Eq(byte[] a, byte[] b)
530 {
531 if (a == b) {
532 return true;
533 }
534 if (a == null || b == null) {
535 return false;
536 }
537 int n = a.Length;
538 if (n != b.Length) {
539 return false;
540 }
541 for (int i = 0; i < n; i ++) {
542 if (a[i] != b[i]) {
543 return false;
544 }
545 }
546 return true;
547 }
548 }
549
550 }