New test.
[mono.git] / mcs / class / Mono.Security / Mono.Security.X509 / X501Name.cs
1 //
2 // X501Name.cs: X.501 Distinguished Names stuff 
3 //
4 // Author:
5 //      Sebastien Pouliot <sebastien@ximian.com>
6 //
7 // (C) 2002, 2003 Motus Technologies Inc. (http://www.motus.com)
8 // Copyright (C) 2004-2006 Novell, Inc (http://www.novell.com)
9 //
10 // Permission is hereby granted, free of charge, to any person obtaining
11 // a copy of this software and associated documentation files (the
12 // "Software"), to deal in the Software without restriction, including
13 // without limitation the rights to use, copy, modify, merge, publish,
14 // distribute, sublicense, and/or sell copies of the Software, and to
15 // permit persons to whom the Software is furnished to do so, subject to
16 // the following conditions:
17 // 
18 // The above copyright notice and this permission notice shall be
19 // included in all copies or substantial portions of the Software.
20 // 
21 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
22 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
24 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
25 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
26 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
27 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28 //
29
30 using System;
31 using System.Globalization;
32 using System.Text;
33
34 using Mono.Security;
35 using Mono.Security.Cryptography;
36
37 namespace Mono.Security.X509 {
38
39         // References:
40         // 1.   Information technology - Open Systems Interconnection - The Directory: Models
41         //      http://www.itu.int/rec/recommendation.asp?type=items&lang=e&parent=T-REC-X.501-200102-I
42         // 2.   RFC2253: Lightweight Directory Access Protocol (v3): UTF-8 String Representation of Distinguished Names
43         //      http://www.ietf.org/rfc/rfc2253.txt
44
45         /*
46          * Name ::= CHOICE { RDNSequence }
47          * 
48          * RDNSequence ::= SEQUENCE OF RelativeDistinguishedName
49          * 
50          * RelativeDistinguishedName ::= SET OF AttributeTypeAndValue
51          */
52 #if INSIDE_CORLIB
53         internal
54 #else
55         public 
56 #endif
57         sealed class X501 {
58
59                 static byte[] countryName = { 0x55, 0x04, 0x06 };
60                 static byte[] organizationName = { 0x55, 0x04, 0x0A };
61                 static byte[] organizationalUnitName = { 0x55, 0x04, 0x0B };
62                 static byte[] commonName = { 0x55, 0x04, 0x03 };
63                 static byte[] localityName = { 0x55, 0x04, 0x07 };
64                 static byte[] stateOrProvinceName = { 0x55, 0x04, 0x08 };
65                 static byte[] streetAddress = { 0x55, 0x04, 0x09 };
66                 //static byte[] serialNumber = { 0x55, 0x04, 0x05 };
67                 static byte[] domainComponent = { 0x09, 0x92, 0x26, 0x89, 0x93, 0xF2, 0x2C, 0x64, 0x01, 0x19 };
68                 static byte[] userid = { 0x09, 0x92, 0x26, 0x89, 0x93, 0xF2, 0x2C, 0x64, 0x01, 0x01 };
69                 static byte[] email = { 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09, 0x01 };
70
71                 private X501 () 
72                 {
73                 }
74
75                 static public string ToString (ASN1 seq) 
76                 {
77                         StringBuilder sb = new StringBuilder ();
78                         for (int i = 0; i < seq.Count; i++) {
79                                 ASN1 entry = seq [i];
80                                 AppendEntry (sb, entry, true);
81
82                                 // separator (not on last iteration)
83                                 if (i < seq.Count - 1)
84                                         sb.Append (", ");
85                         }
86                         return sb.ToString ();
87                 }
88
89                 static public string ToString (ASN1 seq, bool reversed, string separator, bool quotes)
90                 {
91                         StringBuilder sb = new StringBuilder ();
92
93                         if (reversed) {
94                                 for (int i = seq.Count - 1; i >= 0; i--) {
95                                         ASN1 entry = seq [i];
96                                         AppendEntry (sb, entry, quotes);
97
98                                         // separator (not on last iteration)
99                                         if (i > 0)
100                                                 sb.Append (separator);
101                                 }
102                         } else {
103                                 for (int i = 0; i < seq.Count; i++) {
104                                         ASN1 entry = seq [i];
105                                         AppendEntry (sb, entry, quotes);
106
107                                         // separator (not on last iteration)
108                                         if (i < seq.Count - 1)
109                                                 sb.Append (separator);
110                                 }
111                         }
112                         return sb.ToString ();
113                 }
114
115                 static private void AppendEntry (StringBuilder sb, ASN1 entry, bool quotes)
116                 {
117                         // multiple entries are valid
118                         for (int k = 0; k < entry.Count; k++) {
119                                 ASN1 pair = entry [k];
120                                 ASN1 s = pair [1];
121                                 if (s == null)
122                                         continue;
123
124                                 ASN1 poid = pair [0];
125                                 if (poid == null)
126                                         continue;
127
128                                 if (poid.CompareValue (countryName))
129                                         sb.Append ("C=");
130                                 else if (poid.CompareValue (organizationName))
131                                         sb.Append ("O=");
132                                 else if (poid.CompareValue (organizationalUnitName))
133                                         sb.Append ("OU=");
134                                 else if (poid.CompareValue (commonName))
135                                         sb.Append ("CN=");
136                                 else if (poid.CompareValue (localityName))
137                                         sb.Append ("L=");
138                                 else if (poid.CompareValue (stateOrProvinceName))
139                                         sb.Append ("S=");       // NOTE: RFC2253 uses ST=
140                                 else if (poid.CompareValue (streetAddress))
141                                         sb.Append ("STREET=");
142                                 else if (poid.CompareValue (domainComponent))
143                                         sb.Append ("DC=");
144                                 else if (poid.CompareValue (userid))
145                                         sb.Append ("UID=");
146                                 else if (poid.CompareValue (email))
147                                         sb.Append ("E=");       // NOTE: Not part of RFC2253
148                                 else {
149                                         // unknown OID
150                                         sb.Append ("OID.");     // NOTE: Not present as RFC2253
151                                         sb.Append (ASN1Convert.ToOid (poid));
152                                         sb.Append ("=");
153                                 }
154
155                                 string sValue = null;
156                                 // 16bits or 8bits string ? TODO not complete (+special chars!)
157                                 if (s.Tag == 0x1E) {
158                                         // BMPSTRING
159                                         StringBuilder sb2 = new StringBuilder ();
160                                         for (int j = 1; j < s.Value.Length; j += 2)
161                                                 sb2.Append ((char)s.Value[j]);
162                                         sValue = sb2.ToString ();
163                                 } else {
164                                         sValue = Encoding.UTF8.GetString (s.Value);
165                                         // in some cases we must quote (") the value
166                                         // Note: this doesn't seems to conform to RFC2253
167                                         char[] specials = { ',', '+', '"', '\\', '<', '>', ';' };
168                                         if (quotes) {
169                                                 if ((sValue.IndexOfAny (specials, 0, sValue.Length) > 0) ||
170                                                     sValue.StartsWith (" ") || (sValue.EndsWith (" ")))
171                                                         sValue = "\"" + sValue + "\"";
172                                         }
173                                 }
174
175                                 sb.Append (sValue);
176
177                                 // separator (not on last iteration)
178                                 if (k < entry.Count - 1)
179                                         sb.Append (", ");
180                         }
181                 }
182
183                 static private X520.AttributeTypeAndValue GetAttributeFromOid (string attributeType) 
184                 {
185                         string s = attributeType.ToUpper (CultureInfo.InvariantCulture).Trim ();
186                         switch (s) {
187                                 case "C":
188                                         return new X520.CountryName ();
189                                 case "O":
190                                         return new X520.OrganizationName ();
191                                 case "OU":
192                                         return new X520.OrganizationalUnitName ();
193                                 case "CN":
194                                         return new X520.CommonName ();
195                                 case "L":
196                                         return new X520.LocalityName ();
197                                 case "S":       // Microsoft
198                                 case "ST":      // RFC2253
199                                         return new X520.StateOrProvinceName ();
200                                 case "E":       // NOTE: Not part of RFC2253
201                                         return new X520.EmailAddress ();
202                                 case "DC":      // RFC2247
203                                         return new X520.DomainComponent ();
204                                 case "UID":     // RFC1274
205                                         return new X520.UserId ();
206                                 default:
207                                         if (s == "OID.") {
208                                                 // MUST support it but it OID may be without it
209                                                 return new X520.Oid (s.Substring (4));
210                                         } else {
211                                                 if (IsOid (s))
212                                                         return new X520.Oid (s);
213                                                 else
214                                                         return null;
215                                         }
216                         }
217                 }
218
219                 static private bool IsOid (string oid)
220                 {
221                         try {
222                                 ASN1 asn = ASN1Convert.FromOid (oid);
223                                 return (asn.Tag == 0x06);
224                         }
225                         catch {
226                                 return false;
227                         }
228                 }
229
230                 // no quote processing
231                 static private X520.AttributeTypeAndValue ReadAttribute (string value, ref int pos)
232                 {
233                         while ((value[pos] == ' ') && (pos < value.Length))
234                                 pos++;
235
236                         // get '=' position in substring
237                         int equal = value.IndexOf ('=', pos);
238                         if (equal == -1) {
239                                 string msg = Locale.GetText ("No attribute found.");
240                                 throw new FormatException (msg);
241                         }
242
243                         string s = value.Substring (pos, equal - pos);
244                         X520.AttributeTypeAndValue atv = GetAttributeFromOid (s);
245                         if (atv == null) {
246                                 string msg = Locale.GetText ("Unknown attribute '{0}'.");
247                                 throw new FormatException (String.Format (msg, s));
248                         }
249                         pos = equal + 1; // skip the '='
250                         return atv;
251                 }
252
253                 static private bool IsHex (char c)
254                 {
255                         if (Char.IsDigit (c))
256                                 return true;
257                         char up = Char.ToUpper (c, CultureInfo.InvariantCulture);
258                         return ((up >= 'A') && (up <= 'F'));
259                 }
260
261                 static string ReadHex (string value, ref int pos)
262                 {
263                         StringBuilder sb = new StringBuilder ();
264                         // it is (at least an) 8 bits char
265                         sb.Append (value[pos++]);
266                         sb.Append (value[pos]);
267                         // look ahead for a 16 bits char
268                         if ((pos < value.Length - 4) && (value[pos+1] == '\\') && IsHex (value[pos+2])) {
269                                 pos += 2; // pass last char and skip \
270                                 sb.Append (value[pos++]);
271                                 sb.Append (value[pos]);
272                         }
273                         byte[] data = CryptoConvert.FromHex (sb.ToString ());
274                         return Encoding.UTF8.GetString (data);
275                 }
276
277                 static private int ReadEscaped (StringBuilder sb, string value, int pos)
278                 {
279                         switch (value[pos]) {
280                         case '\\':
281                         case '"':
282                         case '=':
283                         case ';':
284                         case '<':
285                         case '>':
286                         case '+':
287                         case '#':
288                         case ',':
289                                 sb.Append (value[pos]);
290                                 return pos;
291                         default:
292                                 if (pos >= value.Length - 2) {
293                                         string msg = Locale.GetText ("Malformed escaped value '{0}'.");
294                                         throw new FormatException (string.Format (msg, value.Substring (pos)));
295                                 }
296                                 // it's either a 8 bits or 16 bits char
297                                 sb.Append (ReadHex (value, ref pos));
298                                 return pos;
299                         }
300                 }
301
302                 static private int ReadQuoted (StringBuilder sb, string value, int pos)
303                 {
304                         int original = pos;
305                         while (pos <= value.Length) {
306                                 switch (value[pos]) {
307                                 case '"':
308                                         return pos;
309                                 case '\\':
310                                         return ReadEscaped (sb, value, pos);
311                                 default:
312                                         sb.Append (value[pos]);
313                                         pos++;
314                                         break;
315                                 }
316                         }
317                         string msg = Locale.GetText ("Malformed quoted value '{0}'.");
318                         throw new FormatException (string.Format (msg, value.Substring (original)));
319                 }
320
321                 static private string ReadValue (string value, ref int pos)
322                 {
323                         int original = pos;
324                         StringBuilder sb = new StringBuilder ();
325                         while (pos < value.Length) {
326                                 switch (value [pos]) {
327                                 case '\\':
328                                         pos = ReadEscaped (sb, value, ++pos);
329                                         break;
330                                 case '"':
331                                         pos = ReadQuoted (sb, value, ++pos);
332                                         break;
333                                 case '=':
334                                 case ';':
335                                 case '<':
336                                 case '>':
337                                         string msg = Locale.GetText ("Malformed value '{0}' contains '{1}' outside quotes.");
338                                         throw new FormatException (string.Format (msg, value.Substring (original), value[pos]));
339                                 case '+':
340                                 case '#':
341                                         throw new NotImplementedException ();
342                                 case ',':
343                                         pos++;
344                                         return sb.ToString ();
345                                 default:
346                                         sb.Append (value[pos]);
347                                         break;
348                                 }
349                                 pos++;
350                         }
351                         return sb.ToString ();
352                 }
353
354                 static public ASN1 FromString (string rdn) 
355                 {
356                         if (rdn == null)
357                                 throw new ArgumentNullException ("rdn");
358
359                         int pos = 0;
360                         ASN1 asn1 = new ASN1 (0x30);
361                         while (pos < rdn.Length) {
362                                 X520.AttributeTypeAndValue atv = ReadAttribute (rdn, ref pos);
363                                 atv.Value = ReadValue (rdn, ref pos);
364
365                                 ASN1 sequence = new ASN1 (0x31);
366                                 sequence.Add (atv.GetASN1 ());
367                                 asn1.Add (sequence); 
368                         }
369                         return asn1;
370                 }
371         }
372 }