mirror of
https://github.com/reactos/reactos.git
synced 2025-05-27 13:08:23 +00:00
508 lines
15 KiB
C++
508 lines
15 KiB
C++
/*
|
|
* PROJECT: ReactOS certutil
|
|
* LICENSE: MIT (https://spdx.org/licenses/MIT)
|
|
* PURPOSE: CertUtil asn implementation
|
|
* COPYRIGHT: Copyright 2020 Mark Jansen (mark.jansen@reactos.org)
|
|
*
|
|
* NOTES:
|
|
* To keep it simple, Tag and Class are combined in one identifier
|
|
* See for more details:
|
|
* https://en.wikipedia.org/wiki/X.690#BER_encoding
|
|
* https://www.strozhevsky.com/free_docs/asn1_by_simple_words.pdf
|
|
* http://mikk.net/~chris/asn1.pdf
|
|
*
|
|
* And for a test suite:
|
|
* https://github.com/YuryStrozhevsky/asn1-test-suite
|
|
*/
|
|
|
|
#include "precomp.h"
|
|
#include <math.h>
|
|
#include <wincrypt.h>
|
|
#include <stdlib.h>
|
|
|
|
|
|
#define ASN_TAG_IS_CONSTRUCTED 0x20
|
|
|
|
|
|
#define ASN_TAG_BITSTRING 0x03
|
|
#define ASN_TAG_OCTET_STRING 0x04
|
|
#define ASN_TAG_OBJECT_ID 0x06
|
|
|
|
#define ASN_TAG_SEQUENCE_RAW 0x10
|
|
#define ASN_TAG_SET_RAW 0x11
|
|
|
|
#define ASN_TAG_SEQUENCE 0x30
|
|
#define ASN_TAG_SET 0x31
|
|
|
|
|
|
#define ASN_TAG_CONTEXT_SPECIFIC 0x80
|
|
#define ASN_TAG_CONTEXT_SPECIFIC_N(n) (ASN_TAG_CONTEXT_SPECIFIC | (n))
|
|
|
|
#define ASN_TAG_OPTIONAL 0xA0
|
|
#define ASN_TAG_OPTIONAL_N(n) (ASN_TAG_OPTIONAL | (n))
|
|
|
|
/* NOTE: These names are not the names listed in f.e. the wikipedia pages,
|
|
they are made to look like MS's names for this */
|
|
LPCWSTR TagToName(DWORD dwTag)
|
|
{
|
|
switch (dwTag)
|
|
{
|
|
case 0x0: return L"EOC";
|
|
case 0x1: return L"BOOL";
|
|
case 0x2: return L"INTEGER";
|
|
case ASN_TAG_BITSTRING: return L"BIT_STRING";
|
|
case ASN_TAG_OCTET_STRING: return L"OCTET_STRING";
|
|
case 0x5: return L"NULL";
|
|
case ASN_TAG_OBJECT_ID: return L"OBJECT_ID";
|
|
case 0x7: return L"Object Descriptor";
|
|
case 0x8: return L"EXTERNAL";
|
|
case 0x9: return L"REAL";
|
|
case 0xA: return L"ENUMERATED";
|
|
case 0xB: return L"EMBEDDED PDV";
|
|
case 0xC: return L"UTF8String";
|
|
case 0xD: return L"RELATIVE-OID";
|
|
case 0xE: return L"TIME";
|
|
case 0xF: return L"Reserved";
|
|
case ASN_TAG_SEQUENCE_RAW: __debugbreak(); return L"SEQUENCE_RAW";
|
|
case ASN_TAG_SET_RAW: __debugbreak(); return L"SET_RAW";
|
|
case 0x12: return L"NumericString";
|
|
case 0x13: return L"PRINTABLE_STRING";
|
|
case 0x14: return L"T61String";
|
|
case 0x15: return L"VideotexString";
|
|
case 0x16: return L"IA5String";
|
|
case 0x17: return L"UTC_TIME";
|
|
case 0x18: return L"GeneralizedTime";
|
|
case 0x19: return L"GraphicString";
|
|
case 0x1A: return L"VisibleString";
|
|
case 0x1B: return L"GeneralString";
|
|
case 0x1C: return L"UniversalString";
|
|
case 0x1D: return L"CHARACTER STRING";
|
|
case 0x1E: return L"BMPString";
|
|
case 0x1F: return L"DATE";
|
|
case 0x20: return L"CONSTRUCTED";
|
|
|
|
case ASN_TAG_SEQUENCE: return L"SEQUENCE";
|
|
case ASN_TAG_SET: return L"SET";
|
|
|
|
|
|
case ASN_TAG_CONTEXT_SPECIFIC_N(0): return L"CONTEXT_SPECIFIC[0]";
|
|
case ASN_TAG_CONTEXT_SPECIFIC_N(1): return L"CONTEXT_SPECIFIC[1]";
|
|
case ASN_TAG_CONTEXT_SPECIFIC_N(2): return L"CONTEXT_SPECIFIC[2]";
|
|
case ASN_TAG_CONTEXT_SPECIFIC_N(3): return L"CONTEXT_SPECIFIC[3]";
|
|
case ASN_TAG_CONTEXT_SPECIFIC_N(4): return L"CONTEXT_SPECIFIC[4]";
|
|
case ASN_TAG_CONTEXT_SPECIFIC_N(5): return L"CONTEXT_SPECIFIC[5]";
|
|
case ASN_TAG_CONTEXT_SPECIFIC_N(6): return L"CONTEXT_SPECIFIC[6]";
|
|
case ASN_TAG_CONTEXT_SPECIFIC_N(7): return L"CONTEXT_SPECIFIC[7]";
|
|
case ASN_TAG_CONTEXT_SPECIFIC_N(8): return L"CONTEXT_SPECIFIC[8]";
|
|
case ASN_TAG_CONTEXT_SPECIFIC_N(9): return L"CONTEXT_SPECIFIC[9]";
|
|
/* Experiments show that Windows' certutil only goes up to 9 */
|
|
|
|
|
|
case ASN_TAG_OPTIONAL_N(0): return L"OPTIONAL[0]";
|
|
case ASN_TAG_OPTIONAL_N(1): return L"OPTIONAL[1]";
|
|
case ASN_TAG_OPTIONAL_N(2): return L"OPTIONAL[2]";
|
|
case ASN_TAG_OPTIONAL_N(3): return L"OPTIONAL[3]";
|
|
case ASN_TAG_OPTIONAL_N(4): return L"OPTIONAL[4]";
|
|
case ASN_TAG_OPTIONAL_N(5): return L"OPTIONAL[5]";
|
|
case ASN_TAG_OPTIONAL_N(6): return L"OPTIONAL[6]";
|
|
case ASN_TAG_OPTIONAL_N(7): return L"OPTIONAL[7]";
|
|
case ASN_TAG_OPTIONAL_N(8): return L"OPTIONAL[8]";
|
|
case ASN_TAG_OPTIONAL_N(9): return L"OPTIONAL[9]";
|
|
/* Experiments show that Windows' certutil only goes up to 9 */
|
|
|
|
default:
|
|
return L"???";
|
|
}
|
|
}
|
|
|
|
BOOL Move(DWORD dwLen, PBYTE& pData, DWORD& dwSize)
|
|
{
|
|
if (dwSize < dwLen)
|
|
return FALSE;
|
|
|
|
pData += dwLen;
|
|
dwSize -= dwLen;
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
BOOL ParseTag(PBYTE& pData, DWORD& dwSize, DWORD& dwTagAndClass)
|
|
{
|
|
if (dwSize == 0)
|
|
return FALSE;
|
|
|
|
/* Is this a long form? */
|
|
if ((pData[0] & 0x1f) != 0x1f)
|
|
{
|
|
/* No, so extract the tag and class (in one identifier) */
|
|
dwTagAndClass = pData[0];
|
|
return Move(1, pData, dwSize);
|
|
}
|
|
|
|
DWORD dwClass = (pData[0] & 0xE0) >> 5;
|
|
dwTagAndClass = 0;
|
|
DWORD n;
|
|
for (n = 1; n < dwSize; ++n)
|
|
{
|
|
dwTagAndClass <<= 7;
|
|
dwTagAndClass |= (pData[n] & 0x7f);
|
|
|
|
if (!(pData[n] & 0x80))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
Move(n, pData, dwSize);
|
|
|
|
/* Any number bigger than this, we shift data out! */
|
|
if (n > 4)
|
|
return FALSE;
|
|
|
|
/* Just drop this in the hightest bits*/
|
|
dwTagAndClass |= (dwClass << (32-3));
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
BOOL ParseLength(PBYTE& pData, DWORD& dwSize, DWORD& dwLength)
|
|
{
|
|
if (dwSize == 0)
|
|
return FALSE;
|
|
|
|
if (!(pData[0] & 0x80))
|
|
{
|
|
dwLength = pData[0];
|
|
return Move(1, pData, dwSize);
|
|
}
|
|
|
|
DWORD dwBytes = pData[0] & 0x7f;
|
|
if (dwBytes == 0 || dwBytes > 8 || dwBytes + 1 > dwSize)
|
|
{
|
|
return FALSE;
|
|
}
|
|
|
|
dwLength = 0;
|
|
for (DWORD n = 0; n < dwBytes; ++n)
|
|
{
|
|
dwLength <<= 8;
|
|
dwLength += pData[1 + n];
|
|
}
|
|
|
|
return Move(dwBytes + 1, pData, dwSize);
|
|
}
|
|
|
|
|
|
DWORD HexDump(PBYTE pRoot, PBYTE pData, DWORD dwSize, PWSTR wszPrefix)
|
|
{
|
|
while (TRUE)
|
|
{
|
|
SIZE_T Address = pData - pRoot;
|
|
ConPrintf(StdOut, L"%04x: ", Address);
|
|
ConPuts(StdOut, wszPrefix);
|
|
|
|
for (DWORD n = 0; n < min(dwSize, 0x10); ++n)
|
|
{
|
|
ConPrintf(StdOut, L"%02x ", pData[n]);
|
|
}
|
|
|
|
if (dwSize <= 0x10)
|
|
break;
|
|
|
|
Move(0x10, pData, dwSize);
|
|
ConPuts(StdOut, L"\n");
|
|
}
|
|
|
|
return 3 * dwSize;
|
|
}
|
|
|
|
void PrintTag(PBYTE pRoot, PBYTE pHeader, DWORD dwTag, DWORD dwTagLength, PBYTE pData, PWSTR wszPrefix)
|
|
{
|
|
DWORD dwRemainder = HexDump(pRoot, pHeader, pData - pHeader, wszPrefix);
|
|
|
|
LPCWSTR wszTag = TagToName(dwTag);
|
|
DWORD dwPadding = dwRemainder + wcslen(wszPrefix);
|
|
while (dwPadding > 50)
|
|
dwPadding -= 50;
|
|
ConPrintf(StdOut, L"%*s; %s (%x Bytes)\n", 50 - dwPadding, L"", wszTag, dwTagLength);
|
|
}
|
|
|
|
struct OID_NAMES
|
|
{
|
|
CHAR* Oid;
|
|
LPCWSTR Names[20];
|
|
DWORD NumberOfNames;
|
|
};
|
|
|
|
BOOL WINAPI CryptOIDEnumCallback(_In_ PCCRYPT_OID_INFO pInfo, _Inout_opt_ void *pvArg)
|
|
{
|
|
OID_NAMES* Names = (OID_NAMES*)pvArg;
|
|
|
|
if (pInfo && pInfo->pszOID && !_stricmp(pInfo->pszOID, Names->Oid))
|
|
{
|
|
if (Names->NumberOfNames < RTL_NUMBER_OF(Names->Names))
|
|
{
|
|
for (DWORD n = 0; n < Names->NumberOfNames; ++n)
|
|
{
|
|
// We already have this..
|
|
if (!_wcsicmp(Names->Names[n], pInfo->pwszName))
|
|
return TRUE;
|
|
}
|
|
|
|
Names->Names[Names->NumberOfNames++] = pInfo->pwszName;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
void PrintOID(PBYTE pRoot, PBYTE pHeader, PBYTE pData, DWORD dwSize, PWSTR wszPrefix)
|
|
{
|
|
/* CryptFindOIDInfo expects the OID to be in ANSI.. */
|
|
CHAR szOID[250];
|
|
CHAR* ptr = szOID;
|
|
size_t cchRemaining = RTL_NUMBER_OF(szOID);
|
|
|
|
/* CryptFindOIDInfo just returns the first, we want multiple */
|
|
OID_NAMES Names = {0};
|
|
|
|
if (dwSize == 0)
|
|
return;
|
|
|
|
DWORD dwValue = 0, count = 0;
|
|
for (DWORD n = 0; n < dwSize; ++n)
|
|
{
|
|
dwValue <<= 7;
|
|
dwValue |= pData[n] & 0x7f;
|
|
|
|
if (pData[n] & 0x80)
|
|
{
|
|
if (++count >= 4)
|
|
break;
|
|
continue;
|
|
}
|
|
count = 0;
|
|
|
|
/* First & second octet have a special encoding */
|
|
if (ptr == szOID)
|
|
{
|
|
DWORD id1 = dwValue / 40;
|
|
DWORD id2 = dwValue % 40;
|
|
|
|
/* The first one can only be 0, 1 or 2, so handle special case: tc24.ber */
|
|
if (id1 > 2)
|
|
{
|
|
id2 += (id1 - 2) * 40;
|
|
id1 = 2;
|
|
}
|
|
StringCchPrintfExA(ptr, cchRemaining, &ptr, &cchRemaining, 0, "%d.%d", id1, id2);
|
|
}
|
|
else
|
|
{
|
|
StringCchPrintfExA(ptr, cchRemaining, &ptr, &cchRemaining, 0, ".%d", dwValue);
|
|
}
|
|
|
|
dwValue = 0;
|
|
}
|
|
|
|
if (dwValue || count)
|
|
{
|
|
/* We cannot format this, so just add abort */
|
|
return;
|
|
}
|
|
|
|
SIZE_T Address = pData - pRoot;
|
|
/* Pad with spaces instead of printing the address again */
|
|
DWORD addrDigits = (DWORD)log10((double)Address) + 1;
|
|
ConPrintf(StdOut, L"%*s ", max(addrDigits, 4), L"");
|
|
ConPrintf(StdOut, L"%s; %S", wszPrefix, szOID);
|
|
|
|
Names.Oid = szOID;
|
|
|
|
/* The order does not match a naive call with '0'... */
|
|
CryptEnumOIDInfo(0, 0, &Names, CryptOIDEnumCallback);
|
|
|
|
for (DWORD n = 0; n < Names.NumberOfNames; ++n)
|
|
{
|
|
if (n == 0)
|
|
ConPrintf(StdOut, L" %s", Names.Names[n]);
|
|
else if (n == 1)
|
|
ConPrintf(StdOut, L" (%s", Names.Names[n]);
|
|
else
|
|
ConPrintf(StdOut, L" / %s", Names.Names[n]);
|
|
}
|
|
|
|
ConPrintf(StdOut, L"%s\n", Names.NumberOfNames > 1 ? L")" : L"");
|
|
}
|
|
|
|
|
|
BOOL ParseAsn(PBYTE pRoot, PBYTE pData, DWORD dwSize, PWSTR wszPrefix, BOOL fPrint)
|
|
{
|
|
while (dwSize)
|
|
{
|
|
PBYTE pHeader = pData;
|
|
DWORD dwTagAndClass;
|
|
|
|
if (!ParseTag(pData, dwSize, dwTagAndClass))
|
|
{
|
|
if (fPrint)
|
|
ConPrintf(StdOut, L"CertUtil: -asn command failed to parse tag near 0x%x\n", pHeader - pRoot);
|
|
return FALSE;
|
|
}
|
|
|
|
DWORD dwTagLength;
|
|
if (!ParseLength(pData, dwSize, dwTagLength))
|
|
{
|
|
if (fPrint)
|
|
ConPrintf(StdOut, L"CertUtil: -asn command failed to parse tag length near 0x%x\n", pHeader - pRoot);
|
|
return FALSE;
|
|
}
|
|
|
|
if (dwTagLength > dwSize)
|
|
{
|
|
if (fPrint)
|
|
ConPrintf(StdOut, L"CertUtil: -asn command malformed tag length near 0x%x\n", pHeader - pRoot);
|
|
return FALSE;
|
|
}
|
|
|
|
|
|
if (fPrint)
|
|
PrintTag(pRoot, pHeader, dwTagAndClass, dwTagLength, pData, wszPrefix);
|
|
|
|
size_t len = wcslen(wszPrefix);
|
|
StringCchCatW(wszPrefix, MAX_PATH, dwTagLength != dwSize ? L"| " : L" ");
|
|
|
|
if (dwTagAndClass & ASN_TAG_IS_CONSTRUCTED)
|
|
{
|
|
if (!ParseAsn(pRoot, pData, dwTagLength, wszPrefix, fPrint))
|
|
{
|
|
return FALSE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (fPrint)
|
|
{
|
|
/* Special case for a bit string / octet string */
|
|
if ((dwTagAndClass == ASN_TAG_BITSTRING || dwTagAndClass == ASN_TAG_OCTET_STRING) && dwTagLength)
|
|
{
|
|
if (dwTagAndClass == ASN_TAG_BITSTRING)
|
|
{
|
|
/* First, we print the 'unused bits' field of the bit string */
|
|
HexDump(pRoot, pData, 1, wszPrefix);
|
|
ConPuts(StdOut, L"\n");
|
|
|
|
/* Move past it */
|
|
Move(1, pData, dwSize);
|
|
dwTagLength--;
|
|
}
|
|
|
|
/* Do we have any data left? */
|
|
if (dwTagLength)
|
|
{
|
|
/* Try to parse this as ASN */
|
|
if (ParseAsn(pRoot, pData, dwTagLength, wszPrefix, FALSE))
|
|
{
|
|
/* We succeeded, this _could_ be ASN, so display it as if it is */
|
|
if (!ParseAsn(pRoot, pData, dwTagLength, wszPrefix, TRUE))
|
|
{
|
|
/* Uhhh, did someone edit the data? */
|
|
ConPrintf(StdOut, L"CertUtil: -asn command unexpected failure parsing tag near 0x%x\n", pData - pRoot);
|
|
return FALSE;
|
|
}
|
|
|
|
/* Move past what we just parsed */
|
|
Move(dwTagLength, pData, dwSize);
|
|
/* Lie about this so that we don't also print a hexdump */
|
|
dwTagLength = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Is there any data (left) to print? */
|
|
if (dwTagLength)
|
|
{
|
|
HexDump(pRoot, pData, dwTagLength, wszPrefix);
|
|
ConPuts(StdOut, L"\n");
|
|
|
|
StringCchCatW(wszPrefix, MAX_PATH, L" ");
|
|
|
|
/* Do we have additional formatters? */
|
|
switch (dwTagAndClass)
|
|
{
|
|
case ASN_TAG_OBJECT_ID:
|
|
PrintOID(pRoot, pHeader, pData, dwTagLength, wszPrefix);
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
wszPrefix[len] = '\0';
|
|
|
|
if (!Move(dwTagLength, pData, dwSize))
|
|
{
|
|
/* This should not be possible, it was checked before! */
|
|
return FALSE;
|
|
}
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
|
|
BOOL asn_dump(LPCWSTR Filename)
|
|
{
|
|
HANDLE hFile = CreateFileW(Filename, GENERIC_READ, FILE_SHARE_READ, NULL,
|
|
OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, NULL);
|
|
|
|
if (hFile == INVALID_HANDLE_VALUE)
|
|
{
|
|
ConPrintf(StdOut, L"CertUtil: -asn command failed to open: %d\n", GetLastError());
|
|
return FALSE;
|
|
}
|
|
|
|
DWORD dwSize = GetFileSize(hFile, NULL);
|
|
if (dwSize == INVALID_FILE_SIZE)
|
|
{
|
|
ConPrintf(StdOut, L"CertUtil: -asn command failed to get file size: %d\n", GetLastError());
|
|
CloseHandle(hFile);
|
|
return FALSE;
|
|
}
|
|
|
|
if (dwSize == 0)
|
|
{
|
|
ConPrintf(StdOut, L"CertUtil: -asn command got an empty file\n");
|
|
CloseHandle(hFile);
|
|
return FALSE;
|
|
}
|
|
|
|
PBYTE pData = (PBYTE)LocalAlloc(0, dwSize);
|
|
if (!pData)
|
|
{
|
|
ConPrintf(StdOut, L"CertUtil: -asn command failed to allocate: %d\n", GetLastError());
|
|
CloseHandle(hFile);
|
|
return FALSE;
|
|
}
|
|
|
|
DWORD cbRead;
|
|
BOOL fRead = ReadFile(hFile, pData, dwSize, &cbRead, NULL);
|
|
DWORD dwErr = GetLastError();
|
|
CloseHandle(hFile);
|
|
|
|
if (!fRead || cbRead != dwSize)
|
|
{
|
|
ConPrintf(StdOut, L"CertUtil: -asn command failed to read: %d\n", dwErr);
|
|
LocalFree(pData);
|
|
return FALSE;
|
|
}
|
|
|
|
WCHAR Buffer[MAX_PATH] = {0};
|
|
BOOL fSucceeded = ParseAsn(pData, pData, dwSize, Buffer, TRUE);
|
|
|
|
LocalFree(pData);
|
|
return fSucceeded;
|
|
}
|
|
|