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;
33 * Helper functions for located BER/DER-encoded object, and in particular
37 public static class AsnIO {
39 public static byte[] FindDER(byte[] buf)
41 return FindBER(buf, true);
44 public static byte[] FindBER(byte[] buf)
46 return FindBER(buf, false);
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
57 * If 'strictDER' is true, then the function furthermore insists
58 * on the object to use a defined DER length.
60 * The returned buffer may be the source buffer itself, or a newly
63 * On error, null is returned.
65 public static byte[] FindBER(byte[] buf, bool strictDER)
67 string pemType = null;
68 return FindBER(buf, strictDER, out pemType);
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
79 * If 'strictDER' is true, then the function furthermore insists
80 * on the object to use a defined DER length.
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.
86 * The returned buffer may be the source buffer itself, or a newly
89 * On error, null is returned.
91 public static byte[] FindBER(byte[] buf,
92 bool strictDER, out string pemType)
97 * If it is already (from the outside) a BER object,
100 if (LooksLikeBER(buf, strictDER)) {
104 string str = BinToString(buf);
110 * Try to detect a PEM header and footer; if we find both
111 * then we remove both, keeping only the characters that
114 int p = str.IndexOf("-----BEGIN ");
115 int q = str.IndexOf("-----END ");
116 if (p >= 0 && q >= 0) {
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);
127 * Convert from Base64.
130 buf = Convert.FromBase64String(str);
131 if (LooksLikeBER(buf, strictDER)) {
135 // ignored: not Base64
145 * Decode multiple PEM objects from a file. Each object is
146 * returned with its name.
148 public static PEMObject[] DecodePEM(byte[] buf)
150 string str = BinToString(buf);
152 return new PEMObject[0];
155 List<PEMObject> fpo = new List<PEMObject>();
156 TextReader tr = new StringReader(str);
157 StringBuilder sb = new StringBuilder();
158 string currentType = null;
160 string line = tr.ReadLine();
164 if (currentType == null) {
165 if (line.StartsWith("-----BEGIN ")) {
166 line = line.Substring(11);
167 int n = line.IndexOf("-----");
169 line = line.Substring(0, n);
171 currentType = string.Copy(line);
172 sb = new StringBuilder();
176 if (line.StartsWith("-----END ")) {
177 string s = sb.ToString();
180 Convert.FromBase64String(s);
181 fpo.Add(new PEMObject(
185 * Base64 decoding failed... we skip
196 return fpo.ToArray();
199 /* =============================================================== */
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
206 static bool DecodeTag(byte[] buf, int lim, ref int off)
213 if ((v & 0x1F) == 0x1F) {
219 } while ((v & 0x80) != 0);
226 * Decode a BER length. Returned value is:
227 * -2 no decodable length
228 * -1 indefinite 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.
233 static int DecodeLength(byte[] buf, int lim, ref int off)
243 } else if (v == 0x80) {
253 if (acc > 0x7FFFFF) {
256 acc = (acc << 8) + buf[p ++];
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.
269 static int BERLength(byte[] buf, int lim, int off)
272 if (!DecodeTag(buf, lim, ref off)) {
275 int len = DecodeLength(buf, lim, ref off);
277 if (len > (lim - off)) {
280 return off + len - orig;
281 } else if (len < -1) {
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.
291 int slen = BERLength(buf, lim, off);
296 if (slen == 2 && buf[off] == 0) {
302 static bool LooksLikeBER(byte[] buf, bool strictDER)
304 return LooksLikeBER(buf, 0, buf.Length, strictDER);
307 static bool LooksLikeBER(byte[] buf, int off, int len, bool strictDER)
310 int objLen = BERLength(buf, lim, off);
315 DecodeTag(buf, lim, ref off);
316 return DecodeLength(buf, lim, ref off) >= 0;
322 static string ConvertMono(byte[] buf, int off)
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) {
333 return new string(tc);
336 static string ConvertBi(byte[] buf, int off, bool be)
338 int len = buf.Length - off;
339 if ((len & 1) != 0) {
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) {
353 return new string(tc);
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.
363 * If conversion is not possible, then null is returned.
365 static string BinToString(byte[] buf)
367 if (buf.Length < 3) {
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);
391 // Starts with UTF-8 BOM
392 str = ConvertMono(buf, 3);
394 // Assumed ASCII-compatible mono-byte encoding
395 str = ConvertMono(buf, 0);