Reactos
1/*
2 * PROJECT: ReactOS certutil
3 * LICENSE: MIT (https://spdx.org/licenses/MIT)
4 * PURPOSE: CertUtil asn implementation
5 * COPYRIGHT: Copyright 2020 Mark Jansen (mark.jansen@reactos.org)
6 *
7 * NOTES:
8 * To keep it simple, Tag and Class are combined in one identifier
9 * See for more details:
10 * https://en.wikipedia.org/wiki/X.690#BER_encoding
11 * https://www.strozhevsky.com/free_docs/asn1_by_simple_words.pdf
12 * http://mikk.net/~chris/asn1.pdf
13 *
14 * And for a test suite:
15 * https://github.com/YuryStrozhevsky/asn1-test-suite
16 */
17
18#include "precomp.h"
19#include <math.h>
20#include <wincrypt.h>
21#include <stdlib.h>
22
23
24#define ASN_TAG_IS_CONSTRUCTED 0x20
25
26
27#define ASN_TAG_BITSTRING 0x03
28#define ASN_TAG_OCTET_STRING 0x04
29#define ASN_TAG_OBJECT_ID 0x06
30
31#define ASN_TAG_SEQUENCE_RAW 0x10
32#define ASN_TAG_SET_RAW 0x11
33
34#define ASN_TAG_SEQUENCE 0x30
35#define ASN_TAG_SET 0x31
36
37
38#define ASN_TAG_CONTEXT_SPECIFIC 0x80
39#define ASN_TAG_CONTEXT_SPECIFIC_N(n) (ASN_TAG_CONTEXT_SPECIFIC | (n))
40
41#define ASN_TAG_OPTIONAL 0xA0
42#define ASN_TAG_OPTIONAL_N(n) (ASN_TAG_OPTIONAL | (n))
43
44/* NOTE: These names are not the names listed in f.e. the wikipedia pages,
45 they are made to look like MS's names for this */
46LPCWSTR TagToName(DWORD dwTag)
47{
48 switch (dwTag)
49 {
50 case 0x0: return L"EOC";
51 case 0x1: return L"BOOL";
52 case 0x2: return L"INTEGER";
53 case ASN_TAG_BITSTRING: return L"BIT_STRING";
54 case ASN_TAG_OCTET_STRING: return L"OCTET_STRING";
55 case 0x5: return L"NULL";
56 case ASN_TAG_OBJECT_ID: return L"OBJECT_ID";
57 case 0x7: return L"Object Descriptor";
58 case 0x8: return L"EXTERNAL";
59 case 0x9: return L"REAL";
60 case 0xA: return L"ENUMERATED";
61 case 0xB: return L"EMBEDDED PDV";
62 case 0xC: return L"UTF8String";
63 case 0xD: return L"RELATIVE-OID";
64 case 0xE: return L"TIME";
65 case 0xF: return L"Reserved";
66 case ASN_TAG_SEQUENCE_RAW: __debugbreak(); return L"SEQUENCE_RAW";
67 case ASN_TAG_SET_RAW: __debugbreak(); return L"SET_RAW";
68 case 0x12: return L"NumericString";
69 case 0x13: return L"PRINTABLE_STRING";
70 case 0x14: return L"T61String";
71 case 0x15: return L"VideotexString";
72 case 0x16: return L"IA5String";
73 case 0x17: return L"UTC_TIME";
74 case 0x18: return L"GeneralizedTime";
75 case 0x19: return L"GraphicString";
76 case 0x1A: return L"VisibleString";
77 case 0x1B: return L"GeneralString";
78 case 0x1C: return L"UniversalString";
79 case 0x1D: return L"CHARACTER STRING";
80 case 0x1E: return L"BMPString";
81 case 0x1F: return L"DATE";
82 case 0x20: return L"CONSTRUCTED";
83
84 case ASN_TAG_SEQUENCE: return L"SEQUENCE";
85 case ASN_TAG_SET: return L"SET";
86
87
88 case ASN_TAG_CONTEXT_SPECIFIC_N(0): return L"CONTEXT_SPECIFIC[0]";
89 case ASN_TAG_CONTEXT_SPECIFIC_N(1): return L"CONTEXT_SPECIFIC[1]";
90 case ASN_TAG_CONTEXT_SPECIFIC_N(2): return L"CONTEXT_SPECIFIC[2]";
91 case ASN_TAG_CONTEXT_SPECIFIC_N(3): return L"CONTEXT_SPECIFIC[3]";
92 case ASN_TAG_CONTEXT_SPECIFIC_N(4): return L"CONTEXT_SPECIFIC[4]";
93 case ASN_TAG_CONTEXT_SPECIFIC_N(5): return L"CONTEXT_SPECIFIC[5]";
94 case ASN_TAG_CONTEXT_SPECIFIC_N(6): return L"CONTEXT_SPECIFIC[6]";
95 case ASN_TAG_CONTEXT_SPECIFIC_N(7): return L"CONTEXT_SPECIFIC[7]";
96 case ASN_TAG_CONTEXT_SPECIFIC_N(8): return L"CONTEXT_SPECIFIC[8]";
97 case ASN_TAG_CONTEXT_SPECIFIC_N(9): return L"CONTEXT_SPECIFIC[9]";
98 /* Experiments show that Windows' certutil only goes up to 9 */
99
100
101 case ASN_TAG_OPTIONAL_N(0): return L"OPTIONAL[0]";
102 case ASN_TAG_OPTIONAL_N(1): return L"OPTIONAL[1]";
103 case ASN_TAG_OPTIONAL_N(2): return L"OPTIONAL[2]";
104 case ASN_TAG_OPTIONAL_N(3): return L"OPTIONAL[3]";
105 case ASN_TAG_OPTIONAL_N(4): return L"OPTIONAL[4]";
106 case ASN_TAG_OPTIONAL_N(5): return L"OPTIONAL[5]";
107 case ASN_TAG_OPTIONAL_N(6): return L"OPTIONAL[6]";
108 case ASN_TAG_OPTIONAL_N(7): return L"OPTIONAL[7]";
109 case ASN_TAG_OPTIONAL_N(8): return L"OPTIONAL[8]";
110 case ASN_TAG_OPTIONAL_N(9): return L"OPTIONAL[9]";
111 /* Experiments show that Windows' certutil only goes up to 9 */
112
113 default:
114 return L"???";
115 }
116}
117
118BOOL Move(DWORD dwLen, PBYTE& pData, DWORD& dwSize)
119{
120 if (dwSize < dwLen)
121 return FALSE;
122
123 pData += dwLen;
124 dwSize -= dwLen;
125
126 return TRUE;
127}
128
129BOOL ParseTag(PBYTE& pData, DWORD& dwSize, DWORD& dwTagAndClass)
130{
131 if (dwSize == 0)
132 return FALSE;
133
134 /* Is this a long form? */
135 if ((pData[0] & 0x1f) != 0x1f)
136 {
137 /* No, so extract the tag and class (in one identifier) */
138 dwTagAndClass = pData[0];
139 return Move(1, pData, dwSize);
140 }
141
142 DWORD dwClass = (pData[0] & 0xE0) >> 5;
143 dwTagAndClass = 0;
144 DWORD n;
145 for (n = 1; n < dwSize; ++n)
146 {
147 dwTagAndClass <<= 7;
148 dwTagAndClass |= (pData[n] & 0x7f);
149
150 if (!(pData[n] & 0x80))
151 {
152 break;
153 }
154 }
155
156 Move(n, pData, dwSize);
157
158 /* Any number bigger than this, we shift data out! */
159 if (n > 4)
160 return FALSE;
161
162 /* Just drop this in the hightest bits*/
163 dwTagAndClass |= (dwClass << (32-3));
164
165 return TRUE;
166}
167
168BOOL ParseLength(PBYTE& pData, DWORD& dwSize, DWORD& dwLength)
169{
170 if (dwSize == 0)
171 return FALSE;
172
173 if (!(pData[0] & 0x80))
174 {
175 dwLength = pData[0];
176 return Move(1, pData, dwSize);
177 }
178
179 DWORD dwBytes = pData[0] & 0x7f;
180 if (dwBytes == 0 || dwBytes > 8 || dwBytes + 1 > dwSize)
181 {
182 return FALSE;
183 }
184
185 dwLength = 0;
186 for (DWORD n = 0; n < dwBytes; ++n)
187 {
188 dwLength <<= 8;
189 dwLength += pData[1 + n];
190 }
191
192 return Move(dwBytes + 1, pData, dwSize);
193}
194
195
196DWORD HexDump(PBYTE pRoot, PBYTE pData, DWORD dwSize, PWSTR wszPrefix)
197{
198 while (TRUE)
199 {
200 SIZE_T Address = pData - pRoot;
201 ConPrintf(StdOut, L"%04x: ", Address);
202 ConPuts(StdOut, wszPrefix);
203
204 for (DWORD n = 0; n < min(dwSize, 0x10); ++n)
205 {
206 ConPrintf(StdOut, L"%02x ", pData[n]);
207 }
208
209 if (dwSize <= 0x10)
210 break;
211
212 Move(0x10, pData, dwSize);
213 ConPuts(StdOut, L"\n");
214 }
215
216 return 3 * dwSize;
217}
218
219void PrintTag(PBYTE pRoot, PBYTE pHeader, DWORD dwTag, DWORD dwTagLength, PBYTE pData, PWSTR wszPrefix)
220{
221 DWORD dwRemainder = HexDump(pRoot, pHeader, pData - pHeader, wszPrefix);
222
223 LPCWSTR wszTag = TagToName(dwTag);
224 DWORD dwPadding = dwRemainder + wcslen(wszPrefix);
225 while (dwPadding > 50)
226 dwPadding -= 50;
227 ConPrintf(StdOut, L"%*s; %s (%x Bytes)\n", 50 - dwPadding, L"", wszTag, dwTagLength);
228}
229
230struct OID_NAMES
231{
232 CHAR* Oid;
233 LPCWSTR Names[20];
234 DWORD NumberOfNames;
235};
236
237BOOL WINAPI CryptOIDEnumCallback(_In_ PCCRYPT_OID_INFO pInfo, _Inout_opt_ void *pvArg)
238{
239 OID_NAMES* Names = (OID_NAMES*)pvArg;
240
241 if (pInfo && pInfo->pszOID && !_stricmp(pInfo->pszOID, Names->Oid))
242 {
243 if (Names->NumberOfNames < RTL_NUMBER_OF(Names->Names))
244 {
245 for (DWORD n = 0; n < Names->NumberOfNames; ++n)
246 {
247 // We already have this..
248 if (!_wcsicmp(Names->Names[n], pInfo->pwszName))
249 return TRUE;
250 }
251
252 Names->Names[Names->NumberOfNames++] = pInfo->pwszName;
253 }
254 }
255
256 return TRUE;
257}
258
259void PrintOID(PBYTE pRoot, PBYTE pHeader, PBYTE pData, DWORD dwSize, PWSTR wszPrefix)
260{
261 /* CryptFindOIDInfo expects the OID to be in ANSI.. */
262 CHAR szOID[250];
263 CHAR* ptr = szOID;
264 size_t cchRemaining = RTL_NUMBER_OF(szOID);
265
266 /* CryptFindOIDInfo just returns the first, we want multiple */
267 OID_NAMES Names = {0};
268
269 if (dwSize == 0)
270 return;
271
272 DWORD dwValue = 0, count = 0;
273 for (DWORD n = 0; n < dwSize; ++n)
274 {
275 dwValue <<= 7;
276 dwValue |= pData[n] & 0x7f;
277
278 if (pData[n] & 0x80)
279 {
280 if (++count >= 4)
281 break;
282 continue;
283 }
284 count = 0;
285
286 /* First & second octet have a special encoding */
287 if (ptr == szOID)
288 {
289 DWORD id1 = dwValue / 40;
290 DWORD id2 = dwValue % 40;
291
292 /* The first one can only be 0, 1 or 2, so handle special case: tc24.ber */
293 if (id1 > 2)
294 {
295 id2 += (id1 - 2) * 40;
296 id1 = 2;
297 }
298 StringCchPrintfExA(ptr, cchRemaining, &ptr, &cchRemaining, 0, "%d.%d", id1, id2);
299 }
300 else
301 {
302 StringCchPrintfExA(ptr, cchRemaining, &ptr, &cchRemaining, 0, ".%d", dwValue);
303 }
304
305 dwValue = 0;
306 }
307
308 if (dwValue || count)
309 {
310 /* We cannot format this, so just add abort */
311 return;
312 }
313
314 SIZE_T Address = pData - pRoot;
315 /* Pad with spaces instead of printing the address again */
316 DWORD addrDigits = (DWORD)log10((double)Address) + 1;
317 ConPrintf(StdOut, L"%*s ", max(addrDigits, 4), L"");
318 ConPrintf(StdOut, L"%s; %S", wszPrefix, szOID);
319
320 Names.Oid = szOID;
321
322 /* The order does not match a naive call with '0'... */
323 CryptEnumOIDInfo(0, 0, &Names, CryptOIDEnumCallback);
324
325 for (DWORD n = 0; n < Names.NumberOfNames; ++n)
326 {
327 if (n == 0)
328 ConPrintf(StdOut, L" %s", Names.Names[n]);
329 else if (n == 1)
330 ConPrintf(StdOut, L" (%s", Names.Names[n]);
331 else
332 ConPrintf(StdOut, L" / %s", Names.Names[n]);
333 }
334
335 ConPrintf(StdOut, L"%s\n", Names.NumberOfNames > 1 ? L")" : L"");
336}
337
338
339BOOL ParseAsn(PBYTE pRoot, PBYTE pData, DWORD dwSize, PWSTR wszPrefix, BOOL fPrint)
340{
341 while (dwSize)
342 {
343 PBYTE pHeader = pData;
344 DWORD dwTagAndClass;
345
346 if (!ParseTag(pData, dwSize, dwTagAndClass))
347 {
348 if (fPrint)
349 ConPrintf(StdOut, L"CertUtil: -asn command failed to parse tag near 0x%x\n", pHeader - pRoot);
350 return FALSE;
351 }
352
353 DWORD dwTagLength;
354 if (!ParseLength(pData, dwSize, dwTagLength))
355 {
356 if (fPrint)
357 ConPrintf(StdOut, L"CertUtil: -asn command failed to parse tag length near 0x%x\n", pHeader - pRoot);
358 return FALSE;
359 }
360
361 if (dwTagLength > dwSize)
362 {
363 if (fPrint)
364 ConPrintf(StdOut, L"CertUtil: -asn command malformed tag length near 0x%x\n", pHeader - pRoot);
365 return FALSE;
366 }
367
368
369 if (fPrint)
370 PrintTag(pRoot, pHeader, dwTagAndClass, dwTagLength, pData, wszPrefix);
371
372 size_t len = wcslen(wszPrefix);
373 StringCchCatW(wszPrefix, MAX_PATH, dwTagLength != dwSize ? L"| " : L" ");
374
375 if (dwTagAndClass & ASN_TAG_IS_CONSTRUCTED)
376 {
377 if (!ParseAsn(pRoot, pData, dwTagLength, wszPrefix, fPrint))
378 {
379 return FALSE;
380 }
381 }
382 else
383 {
384 if (fPrint)
385 {
386 /* Special case for a bit string / octet string */
387 if ((dwTagAndClass == ASN_TAG_BITSTRING || dwTagAndClass == ASN_TAG_OCTET_STRING) && dwTagLength)
388 {
389 if (dwTagAndClass == ASN_TAG_BITSTRING)
390 {
391 /* First, we print the 'unused bits' field of the bit string */
392 HexDump(pRoot, pData, 1, wszPrefix);
393 ConPuts(StdOut, L"\n");
394
395 /* Move past it */
396 Move(1, pData, dwSize);
397 dwTagLength--;
398 }
399
400 /* Do we have any data left? */
401 if (dwTagLength)
402 {
403 /* Try to parse this as ASN */
404 if (ParseAsn(pRoot, pData, dwTagLength, wszPrefix, FALSE))
405 {
406 /* We succeeded, this _could_ be ASN, so display it as if it is */
407 if (!ParseAsn(pRoot, pData, dwTagLength, wszPrefix, TRUE))
408 {
409 /* Uhhh, did someone edit the data? */
410 ConPrintf(StdOut, L"CertUtil: -asn command unexpected failure parsing tag near 0x%x\n", pData - pRoot);
411 return FALSE;
412 }
413
414 /* Move past what we just parsed */
415 Move(dwTagLength, pData, dwSize);
416 /* Lie about this so that we don't also print a hexdump */
417 dwTagLength = 0;
418 }
419 }
420 }
421
422 /* Is there any data (left) to print? */
423 if (dwTagLength)
424 {
425 HexDump(pRoot, pData, dwTagLength, wszPrefix);
426 ConPuts(StdOut, L"\n");
427
428 StringCchCatW(wszPrefix, MAX_PATH, L" ");
429
430 /* Do we have additional formatters? */
431 switch (dwTagAndClass)
432 {
433 case ASN_TAG_OBJECT_ID:
434 PrintOID(pRoot, pHeader, pData, dwTagLength, wszPrefix);
435 break;
436 default:
437 break;
438 }
439 }
440 }
441 }
442
443 wszPrefix[len] = '\0';
444
445 if (!Move(dwTagLength, pData, dwSize))
446 {
447 /* This should not be possible, it was checked before! */
448 return FALSE;
449 }
450 }
451
452 return TRUE;
453}
454
455
456BOOL asn_dump(LPCWSTR Filename)
457{
458 HANDLE hFile = CreateFileW(Filename, GENERIC_READ, FILE_SHARE_READ, NULL,
459 OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);
460
461 if (hFile == INVALID_HANDLE_VALUE)
462 {
463 ConPrintf(StdOut, L"CertUtil: -asn command failed to open: %d\n", GetLastError());
464 return FALSE;
465 }
466
467 DWORD dwSize = GetFileSize(hFile, NULL);
468 if (dwSize == INVALID_FILE_SIZE)
469 {
470 ConPrintf(StdOut, L"CertUtil: -asn command failed to get file size: %d\n", GetLastError());
471 CloseHandle(hFile);
472 return FALSE;
473 }
474
475 if (dwSize == 0)
476 {
477 ConPrintf(StdOut, L"CertUtil: -asn command got an empty file\n");
478 CloseHandle(hFile);
479 return FALSE;
480 }
481
482 PBYTE pData = (PBYTE)LocalAlloc(0, dwSize);
483 if (!pData)
484 {
485 ConPrintf(StdOut, L"CertUtil: -asn command failed to allocate: %d\n", GetLastError());
486 CloseHandle(hFile);
487 return FALSE;
488 }
489
490 DWORD cbRead;
491 BOOL fRead = ReadFile(hFile, pData, dwSize, &cbRead, NULL);
492 DWORD dwErr = GetLastError();
493 CloseHandle(hFile);
494
495 if (!fRead || cbRead != dwSize)
496 {
497 ConPrintf(StdOut, L"CertUtil: -asn command failed to read: %d\n", dwErr);
498 LocalFree(pData);
499 return FALSE;
500 }
501
502 WCHAR Buffer[MAX_PATH] = {0};
503 BOOL fSucceeded = ParseAsn(pData, pData, dwSize, Buffer, TRUE);
504
505 LocalFree(pData);
506 return fSucceeded;
507}
508