Initial commit.
[BoarSSL] / Asn1 / AsnIO.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 namespace Asn1 {
31
32 /*
33 * Helper functions for located BER/DER-encoded object, and in particular
34 * handle PEM format.
35 */
36
37 public static class AsnIO {
38
39 public static byte[] FindDER(byte[] buf)
40 {
41 return FindBER(buf, true);
42 }
43
44 public static byte[] FindBER(byte[] buf)
45 {
46 return FindBER(buf, false);
47 }
48
49 /*
50 * Find a BER/DER object in the provided buffer. If the data is
51 * not already in the right format, conversion to string then
52 * Base64 decoding is attempted; in the latter case, PEM headers
53 * are detected and skipped. In any case, the returned buffer
54 * must begin with a well-formed tag and length, corresponding to
55 * the object length.
56 *
57 * If 'strictDER' is true, then the function furthermore insists
58 * on the object to use a defined DER length.
59 *
60 * The returned buffer may be the source buffer itself, or a newly
61 * allocated buffer.
62 *
63 * On error, null is returned.
64 */
65 public static byte[] FindBER(byte[] buf, bool strictDER)
66 {
67 string pemType = null;
68 return FindBER(buf, strictDER, out pemType);
69 }
70
71 /*
72 * Find a BER/DER object in the provided buffer. If the data is
73 * not already in the right format, conversion to string then
74 * Base64 decoding is attempted; in the latter case, PEM headers
75 * are detected and skipped. In any case, the returned buffer
76 * must begin with a well-formed tag and length, corresponding to
77 * the object length.
78 *
79 * If 'strictDER' is true, then the function furthermore insists
80 * on the object to use a defined DER length.
81 *
82 * If the source was detected to use PEM, then the object type
83 * indicated by the PEM header is written in 'pemType'; otherwise,
84 * that variable is set to null.
85 *
86 * The returned buffer may be the source buffer itself, or a newly
87 * allocated buffer.
88 *
89 * On error, null is returned.
90 */
91 public static byte[] FindBER(byte[] buf,
92 bool strictDER, out string pemType)
93 {
94 pemType = null;
95
96 /*
97 * If it is already (from the outside) a BER object,
98 * return it.
99 */
100 if (LooksLikeBER(buf, strictDER)) {
101 return buf;
102 }
103
104 string str = BinToString(buf);
105 if (str == null) {
106 return null;
107 }
108
109 /*
110 * Try to detect a PEM header and footer; if we find both
111 * then we remove both, keeping only the characters that
112 * occur in between.
113 */
114 int p = str.IndexOf("-----BEGIN ");
115 int q = str.IndexOf("-----END ");
116 if (p >= 0 && q >= 0) {
117 p += 11;
118 int r = str.IndexOf((char)10, p) + 1;
119 int px = str.IndexOf('-', p);
120 if (px > 0 && px < r && r > 0 && r <= q) {
121 pemType = string.Copy(str.Substring(p, px - p));
122 str = str.Substring(r, q - r);
123 }
124 }
125
126 /*
127 * Convert from Base64.
128 */
129 try {
130 buf = Convert.FromBase64String(str);
131 if (LooksLikeBER(buf, strictDER)) {
132 return buf;
133 }
134 } catch {
135 // ignored: not Base64
136 }
137
138 /*
139 * Decoding failed.
140 */
141 return null;
142 }
143
144 /*
145 * Decode multiple PEM objects from a file. Each object is
146 * returned with its name.
147 */
148 public static PEMObject[] DecodePEM(byte[] buf)
149 {
150 string str = BinToString(buf);
151 if (str == null) {
152 return new PEMObject[0];
153 }
154
155 List<PEMObject> fpo = new List<PEMObject>();
156 TextReader tr = new StringReader(str);
157 StringBuilder sb = new StringBuilder();
158 string currentType = null;
159 for (;;) {
160 string line = tr.ReadLine();
161 if (line == null) {
162 break;
163 }
164 if (currentType == null) {
165 if (line.StartsWith("-----BEGIN ")) {
166 line = line.Substring(11);
167 int n = line.IndexOf("-----");
168 if (n >= 0) {
169 line = line.Substring(0, n);
170 }
171 currentType = string.Copy(line);
172 sb = new StringBuilder();
173 }
174 continue;
175 }
176 if (line.StartsWith("-----END ")) {
177 string s = sb.ToString();
178 try {
179 byte[] data =
180 Convert.FromBase64String(s);
181 fpo.Add(new PEMObject(
182 currentType, data));
183 } catch {
184 /*
185 * Base64 decoding failed... we skip
186 * the object.
187 */
188 }
189 currentType = null;
190 sb.Clear();
191 continue;
192 }
193 sb.Append(line);
194 sb.Append("\n");
195 }
196 return fpo.ToArray();
197 }
198
199 /* =============================================================== */
200
201 /*
202 * Decode a tag; returned value is true on success, false otherwise.
203 * On success, 'off' is updated to point to the first byte after
204 * the tag.
205 */
206 static bool DecodeTag(byte[] buf, int lim, ref int off)
207 {
208 int p = off;
209 if (p >= lim) {
210 return false;
211 }
212 int v = buf[p ++];
213 if ((v & 0x1F) == 0x1F) {
214 do {
215 if (p >= lim) {
216 return false;
217 }
218 v = buf[p ++];
219 } while ((v & 0x80) != 0);
220 }
221 off = p;
222 return true;
223 }
224
225 /*
226 * Decode a BER length. Returned value is:
227 * -2 no decodable length
228 * -1 indefinite length
229 * 0+ definite length
230 * If a definite or indefinite length could be decoded, then 'off'
231 * is updated to point to the first byte after the length.
232 */
233 static int DecodeLength(byte[] buf, int lim, ref int off)
234 {
235 int p = off;
236 if (p >= lim) {
237 return -2;
238 }
239 int v = buf[p ++];
240 if (v < 0x80) {
241 off = p;
242 return v;
243 } else if (v == 0x80) {
244 off = p;
245 return -1;
246 }
247 v &= 0x7F;
248 if ((lim - p) < v) {
249 return -2;
250 }
251 int acc = 0;
252 while (v -- > 0) {
253 if (acc > 0x7FFFFF) {
254 return -2;
255 }
256 acc = (acc << 8) + buf[p ++];
257 }
258 off = p;
259 return acc;
260 }
261
262 /*
263 * Get the length, in bytes, of the object in the provided
264 * buffer. The object begins at offset 'off' but does not extend
265 * farther than offset 'lim'. If no such BER object can be
266 * decoded, then -1 is returned. The returned length includes
267 * that of the tag and length fields.
268 */
269 static int BERLength(byte[] buf, int lim, int off)
270 {
271 int orig = off;
272 if (!DecodeTag(buf, lim, ref off)) {
273 return -1;
274 }
275 int len = DecodeLength(buf, lim, ref off);
276 if (len >= 0) {
277 if (len > (lim - off)) {
278 return -1;
279 }
280 return off + len - orig;
281 } else if (len < -1) {
282 return -1;
283 }
284
285 /*
286 * Indefinite length: we must do some recursive exploration.
287 * End of structure is marked by a "null tag": object has
288 * total length 2 and its tag byte is 0.
289 */
290 for (;;) {
291 int slen = BERLength(buf, lim, off);
292 if (slen < 0) {
293 return -1;
294 }
295 off += slen;
296 if (slen == 2 && buf[off] == 0) {
297 return off - orig;
298 }
299 }
300 }
301
302 static bool LooksLikeBER(byte[] buf, bool strictDER)
303 {
304 return LooksLikeBER(buf, 0, buf.Length, strictDER);
305 }
306
307 static bool LooksLikeBER(byte[] buf, int off, int len, bool strictDER)
308 {
309 int lim = off + len;
310 int objLen = BERLength(buf, lim, off);
311 if (objLen != len) {
312 return false;
313 }
314 if (strictDER) {
315 DecodeTag(buf, lim, ref off);
316 return DecodeLength(buf, lim, ref off) >= 0;
317 } else {
318 return true;
319 }
320 }
321
322 static string ConvertMono(byte[] buf, int off)
323 {
324 int len = buf.Length - off;
325 char[] tc = new char[len];
326 for (int i = 0; i < len; i ++) {
327 int v = buf[off + i];
328 if (v < 1 || v > 126) {
329 v = '?';
330 }
331 tc[i] = (char)v;
332 }
333 return new string(tc);
334 }
335
336 static string ConvertBi(byte[] buf, int off, bool be)
337 {
338 int len = buf.Length - off;
339 if ((len & 1) != 0) {
340 return null;
341 }
342 len >>= 1;
343 char[] tc = new char[len];
344 for (int i = 0; i < len; i ++) {
345 int b0 = buf[off + (i << 1) + 0];
346 int b1 = buf[off + (i << 1) + 1];
347 int v = be ? ((b0 << 8) + b1) : (b0 + (b1 << 8));
348 if (v < 1 || v > 126) {
349 v = '?';
350 }
351 tc[i] = (char)v;
352 }
353 return new string(tc);
354 }
355
356 /*
357 * Convert a blob to a string. This supports UTF-16 (with and
358 * without a BOM), UTF-8 (with and without a BOM), and
359 * ASCII-compatible encodings. Non-ASCII characters get
360 * replaced with '?'. This function is meant to be used
361 * with heuristic PEM decoders.
362 *
363 * If conversion is not possible, then null is returned.
364 */
365 static string BinToString(byte[] buf)
366 {
367 if (buf.Length < 3) {
368 return null;
369 }
370 string str = null;
371 if ((buf.Length & 1) == 0) {
372 if (buf[0] == 0xFE && buf[1] == 0xFF) {
373 // Starts with big-endian UTF-16 BOM
374 str = ConvertBi(buf, 2, true);
375 } else if (buf[0] == 0xFF && buf[1] == 0xFE) {
376 // Starts with little-endian UTF-16 BOM
377 str = ConvertBi(buf, 2, false);
378 } else if (buf[0] == 0) {
379 // First byte is 0 -> big-endian UTF-16
380 str = ConvertBi(buf, 0, true);
381 } else if (buf[1] == 0) {
382 // Second byte is 0 -> little-endian UTF-16
383 str = ConvertBi(buf, 0, false);
384 }
385 }
386 if (str == null) {
387 if (buf[0] == 0xEF
388 && buf[1] == 0xBB
389 && buf[2] == 0xBF)
390 {
391 // Starts with UTF-8 BOM
392 str = ConvertMono(buf, 3);
393 } else {
394 // Assumed ASCII-compatible mono-byte encoding
395 str = ConvertMono(buf, 0);
396 }
397 }
398 return str;
399 }
400 }
401
402 }