2 * Copyright (c) 2017 Thomas Pornin <pornin@bolet.org>
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:
12 * The above copyright notice and this permission notice shall be
13 * included in all copies or substantial portions of the Software.
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
26 using System.Collections.Generic;
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).
45 * These are the known "friendly types". The values decode as
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";
61 * Get the type OID (decimal-dotted string representation).
71 * Get the string value for this element. If the element value
72 * could not be decoded as a string, then this method returns
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.)
87 * Tell whether this element is string based. This property
88 * returns true if and only if Value returns a non-null value.
90 public bool IsString {
92 return Value_ != null;
97 * Get the element value as an ASN.1 structure.
99 public AsnElt AsnValue {
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).
112 public string FriendlyType {
114 return GetFriendlyType(OID);
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.
124 * This does not implement full RFC 4518 rules, but it should
125 * be good enough for an analysis tool.
132 internal DNPart(string oid, AsnElt val)
136 encodedValue = val.Encode();
137 uint hc = (uint)oid.GetHashCode();
139 string s = val.GetString();
141 s = s.ToUpperInvariant().ToLowerInvariant();
142 StringBuilder sb = new StringBuilder();
144 foreach (char c in s.Trim()) {
159 if (n > 0 && sb[n - 1] == ' ') {
162 normValue = sb.ToString();
163 hc += (uint)normValue.GetHashCode();
165 if (OID_TO_FT.ContainsKey(oid)) {
169 foreach (byte b in encodedValue) {
170 hc = ((hc << 7) | (hc >> 25)) ^ (uint)b;
176 static bool MustEscape(int x)
178 if (x < 0x20 || x >= 0x7F) {
196 * Convert this element to a string. This uses RFC 4514 rules.
198 public override string ToString()
200 StringBuilder sb = new StringBuilder();
202 if (OID_TO_FT.TryGetValue(OID, out ft) && IsString) {
205 byte[] buf = Encoding.UTF8.GetBytes(Value);
206 for (int i = 0; i < buf.Length; i ++) {
208 if ((i == 0 && (b == ' ' || b == '#'))
209 || (i == buf.Length - 1 && b == ' ')
227 sb.AppendFormat("\\{0:X2}", b);
237 foreach (byte b in AsnValue.Encode()) {
238 sb.AppendFormat("{0:X2}", b);
241 return sb.ToString();
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.
249 public static string GetFriendlyType(string oid)
252 if (OID_TO_FT.TryGetValue(oid, out ft)) {
258 static int HexVal(char c)
260 if (c >= '0' && c <= '9') {
262 } else if (c >= 'A' && c <= 'F') {
263 return c - ('A' - 10);
264 } else if (c >= 'a' && c <= 'f') {
265 return c - ('a' - 10);
271 static int HexValCheck(char c)
275 throw new AsnException(String.Format(
276 "Not an hex digit: U+{0:X4}", c));
281 static int HexVal2(string str, int k)
283 if (k >= str.Length) {
284 throw new AsnException("Missing hex digits");
286 int x = HexVal(str[k]);
287 if ((k + 1) >= str.Length) {
288 throw new AsnException("Odd number of hex digits");
290 return (x << 4) + HexVal(str[k + 1]);
293 static int ReadHexEscape(string str, ref int off)
295 if (off >= str.Length || str[off] != '\\') {
298 if ((off + 1) >= str.Length) {
299 throw new AsnException("Truncated escape");
301 int x = HexVal(str[off + 1]);
305 if ((off + 2) >= str.Length) {
306 throw new AsnException("Truncated escape");
308 int y = HexValCheck(str[off + 2]);
313 static int ReadHexUTF(string str, ref int off)
315 int x = ReadHexEscape(str, ref off);
316 if (x < 0x80 || x >= 0xC0) {
317 throw new AsnException(
318 "Invalid hex escape: not UTF-8");
323 static string UnEscapeUTF8(string str)
325 StringBuilder sb = new StringBuilder();
335 int x = ReadHexEscape(str, ref k);
337 sb.Append(str[k + 1]);
343 } else if (x < 0xC0) {
344 throw new AsnException(
345 "Invalid hex escape: not UTF-8");
346 } else if (x < 0xE0) {
348 x = (x << 6) | ReadHexUTF(str, ref k) & 0x3F;
349 } else if (x < 0xF0) {
351 x = (x << 6) | ReadHexUTF(str, ref k) & 0x3F;
352 x = (x << 6) | ReadHexUTF(str, ref k) & 0x3F;
353 } else if (x < 0xF8) {
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;
359 throw new AsnException("Invalid"
360 + " hex escape: out of range");
363 throw new AsnException(
364 "Invalid hex escape: not UTF-8");
370 sb.Append((char)(0xD800 + (x >> 10)));
371 sb.Append((char)(0xDC00 + (x & 0x3FF)));
374 return sb.ToString();
377 internal static DNPart Parse(string str)
379 int j = str.IndexOf('=');
381 throw new AsnException("Invalid DN: no '=' sign");
383 string a = str.Substring(0, j).Trim();
384 string b = str.Substring(j + 1).Trim();
386 if (!FT_TO_OID.TryGetValue(a, out oid)) {
387 oid = AsnElt.MakeOID(oid).GetOID();
390 if (b.StartsWith("#")) {
391 MemoryStream ms = new MemoryStream();
393 for (int k = 1; k < n; k += 2) {
394 int x = HexValCheck(b[k]);
396 throw new AsnException(
397 "Odd number of hex digits");
399 x = (x << 4) + HexValCheck(b[k + 1]);
400 ms.WriteByte((byte)x);
403 aVal = AsnElt.Decode(ms.ToArray());
404 } catch (Exception e) {
405 throw new AsnException("Bad DN value: "
410 int type = AsnElt.PrintableString;
411 foreach (char c in b) {
412 if (!AsnElt.IsPrintable(c)) {
413 type = AsnElt.UTF8String;
417 aVal = AsnElt.MakeString(type, b);
419 return new DNPart(oid, aVal);
422 static Dictionary<string, string> OID_TO_FT;
423 static Dictionary<string, string> FT_TO_OID;
425 static void AddFT(string oid, string ft)
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);
448 * We also accept 'S' as an alias for 'ST' because some
449 * Microsoft software uses it.
451 FT_TO_OID["S"] = FT_TO_OID["ST"];
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.
459 static bool IsControl(char c)
462 || (c >= 0x000E && c <= 0x001F)
463 || (c >= 0x007F && c <= 0x0084)
464 || (c >= 0x0086 && c <= 0x009F)
468 || (c >= 0x200C && c <= 0x200F)
469 || (c >= 0x202A && c <= 0x202E)
470 || (c >= 0x2060 && c <= 0x2063)
471 || (c >= 0x206A && c <= 0x206F)
473 || (c >= 0xFFF9 && c <= 0xFFFB))
481 * Tell whether a character is whitespace. This follows
484 static bool IsWS(char c)
489 || (c >= 0x2000 && c <= 0x200A)
501 public override bool Equals(object obj)
503 return Equals(obj as DNPart);
506 public bool Equals(DNPart dnp)
511 if (OID != dnp.OID) {
516 && normValue == dnp.normValue;
517 } else if (dnp.IsString) {
520 return Eq(encodedValue, dnp.encodedValue);
524 public override int GetHashCode()
529 static bool Eq(byte[] a, byte[] b)
534 if (a == null || b == null) {
541 for (int i = 0; i < n; i ++) {