518 lines
16 KiB
C++
518 lines
16 KiB
C++
|
//+-------------------------------------------------------------------------
|
||
|
// Microsoft Windows
|
||
|
//
|
||
|
// Copyright (C) Microsoft Corporation, 2001 - 2001
|
||
|
//
|
||
|
// File: asn1util.cpp
|
||
|
//
|
||
|
// Contents: Minimal ASN.1 utility helper functions.
|
||
|
//
|
||
|
// Functions: MinAsn1DecodeLength
|
||
|
// MinAsn1ExtractContent
|
||
|
// MinAsn1ExtractValues
|
||
|
//
|
||
|
// MinAsn1FindExtension
|
||
|
// MinAsn1FindAttribute
|
||
|
// MinAsn1ExtractParsedCertificatesFromSignedData
|
||
|
//
|
||
|
// History: 15-Jan-01 philh created
|
||
|
//--------------------------------------------------------------------------
|
||
|
|
||
|
#include "global.hxx"
|
||
|
|
||
|
//+-------------------------------------------------------------------------
|
||
|
// Get the number of contents octets in a definite-length BER-encoding.
|
||
|
//
|
||
|
// Parameters:
|
||
|
// pcbContent - receives the number of contents octets
|
||
|
// pbLength - points to the first length octet
|
||
|
// cbBER - number of bytes remaining in the BER encoding
|
||
|
//
|
||
|
// Returns:
|
||
|
// success - the number of bytes in the length field, > 0
|
||
|
// failure - < 0
|
||
|
//
|
||
|
// One of the following failure values can be returned:
|
||
|
// MINASN1_LENGTH_TOO_LARGE
|
||
|
// MINASN1_INSUFFICIENT_DATA
|
||
|
// MINASN1_UNSUPPORTED_INDEFINITE_LENGTH
|
||
|
//--------------------------------------------------------------------------
|
||
|
LONG
|
||
|
WINAPI
|
||
|
MinAsn1DecodeLength(
|
||
|
OUT DWORD *pcbContent,
|
||
|
IN const BYTE *pbLength,
|
||
|
IN DWORD cbBER)
|
||
|
{
|
||
|
long i;
|
||
|
BYTE cbLength;
|
||
|
const BYTE *pb;
|
||
|
|
||
|
if (cbBER < 1)
|
||
|
goto TooLittleData;
|
||
|
|
||
|
if (0x80 == *pbLength)
|
||
|
goto IndefiniteLength;
|
||
|
|
||
|
// determine the number of length octets and contents octets
|
||
|
if ((cbLength = *pbLength) & 0x80) {
|
||
|
cbLength &= ~0x80; // low 7 bits have number of bytes
|
||
|
if (cbLength > 4)
|
||
|
goto LengthTooLargeError;
|
||
|
if (cbLength >= cbBER)
|
||
|
goto TooLittleData;
|
||
|
*pcbContent = 0;
|
||
|
for (i=cbLength, pb=pbLength+1; i>0; i--, pb++)
|
||
|
*pcbContent = (*pcbContent << 8) + (const DWORD)*pb;
|
||
|
i = cbLength + 1;
|
||
|
} else {
|
||
|
*pcbContent = (DWORD)cbLength;
|
||
|
i = 1;
|
||
|
}
|
||
|
|
||
|
CommonReturn:
|
||
|
return i; // how many bytes there were in the length field
|
||
|
|
||
|
LengthTooLargeError:
|
||
|
i = MINASN1_LENGTH_TOO_LARGE;
|
||
|
goto CommonReturn;
|
||
|
|
||
|
IndefiniteLength:
|
||
|
i = MINASN1_UNSUPPORTED_INDEFINITE_LENGTH;
|
||
|
goto CommonReturn;
|
||
|
|
||
|
TooLittleData:
|
||
|
i = MINASN1_INSUFFICIENT_DATA;
|
||
|
goto CommonReturn;
|
||
|
}
|
||
|
|
||
|
|
||
|
//+-------------------------------------------------------------------------
|
||
|
// Point to the content octets in a definite-length BER-encoded blob.
|
||
|
//
|
||
|
// Returns:
|
||
|
// success - the number of bytes skipped, > 0
|
||
|
// failure - < 0
|
||
|
//
|
||
|
// One of the following failure values can be returned:
|
||
|
// MINASN1_LENGTH_TOO_LARGE
|
||
|
// MINASN1_INSUFFICIENT_DATA
|
||
|
// MINASN1_UNSUPPORTED_INDEFINITE_LENGTH
|
||
|
//
|
||
|
// Assumption: pbData points to a definite-length BER-encoded blob.
|
||
|
// If *pcbContent isn't within cbBER, MINASN1_INSUFFICIENT_DATA
|
||
|
// is returned.
|
||
|
//--------------------------------------------------------------------------
|
||
|
LONG
|
||
|
WINAPI
|
||
|
MinAsn1ExtractContent(
|
||
|
IN const BYTE *pbBER,
|
||
|
IN DWORD cbBER,
|
||
|
OUT DWORD *pcbContent,
|
||
|
OUT const BYTE **ppbContent)
|
||
|
{
|
||
|
#define TAG_MASK 0x1f
|
||
|
DWORD cbIdentifier;
|
||
|
DWORD cbContent;
|
||
|
LONG cbLength;
|
||
|
LONG lHeader;
|
||
|
const BYTE *pb = pbBER;
|
||
|
|
||
|
if (0 == cbBER--)
|
||
|
goto TooLittleData;
|
||
|
|
||
|
// Skip over the identifier octet(s)
|
||
|
if (TAG_MASK == (*pb++ & TAG_MASK)) {
|
||
|
// high-tag-number form
|
||
|
cbIdentifier = 2;
|
||
|
while (TRUE) {
|
||
|
if (0 == cbBER--)
|
||
|
goto TooLittleData;
|
||
|
if (0 == (*pb++ & 0x80))
|
||
|
break;
|
||
|
cbIdentifier++;
|
||
|
}
|
||
|
} else {
|
||
|
// low-tag-number form
|
||
|
cbIdentifier = 1;
|
||
|
}
|
||
|
|
||
|
if (0 > (cbLength = MinAsn1DecodeLength( &cbContent, pb, cbBER))) {
|
||
|
lHeader = cbLength;
|
||
|
goto CommonReturn;
|
||
|
}
|
||
|
|
||
|
if (cbContent > (cbBER - cbLength))
|
||
|
goto TooLittleData;
|
||
|
|
||
|
pb += cbLength;
|
||
|
|
||
|
*pcbContent = cbContent;
|
||
|
*ppbContent = pb;
|
||
|
|
||
|
lHeader = cbLength + cbIdentifier;
|
||
|
CommonReturn:
|
||
|
return lHeader;
|
||
|
|
||
|
TooLittleData:
|
||
|
lHeader = MINASN1_INSUFFICIENT_DATA;
|
||
|
goto CommonReturn;
|
||
|
}
|
||
|
|
||
|
|
||
|
typedef struct _STEP_INTO_STACK_ENTRY {
|
||
|
const BYTE *pb;
|
||
|
DWORD cb;
|
||
|
BOOL fSkipIntoValues;
|
||
|
} STEP_INTO_STACK_ENTRY, *PSTEP_INTO_STACK_ENTRY;
|
||
|
|
||
|
#define MAX_STEP_INTO_DEPTH 8
|
||
|
|
||
|
//+-------------------------------------------------------------------------
|
||
|
// Extract one or more tagged values from the ASN.1 encoded byte array.
|
||
|
//
|
||
|
// Either steps into the value's content octets (MINASN1_STEP_INTO_VALUE_OP or
|
||
|
// MINASN1_OPTIONAL_STEP_INTO_VALUE_OP) or steps over the value's tag,
|
||
|
// length and content octets (MINASN1_STEP_OVER_VALUE_OP or
|
||
|
// MINASN1_OPTIONAL_STEP_OVER_VALUE_OP).
|
||
|
//
|
||
|
// You can step out of a stepped into sequence via MINASN1_STEP_OUT_VALUE_OP.
|
||
|
//
|
||
|
// For tag matching, only supports single byte tags.
|
||
|
//
|
||
|
// Only definite-length ASN.1 is supported.
|
||
|
//
|
||
|
// *pcValue is updated with the number of values successfully extracted.
|
||
|
//
|
||
|
// Returns:
|
||
|
// success - >= 0 => length of all bytes consumed through the last value
|
||
|
// extracted. For STEP_INTO, only the tag and length
|
||
|
// octets.
|
||
|
// failure - < 0 => negative (offset + 1) of first bad tagged value
|
||
|
//
|
||
|
// A non-NULL rgValueBlob[] is updated with the pointer to and length of the
|
||
|
// tagged value or its content octets. The rgValuePara[].dwIndex is used to
|
||
|
// index into rgValueBlob[]. For OPTIONAL_STEP_OVER or
|
||
|
// OPTIONAL_STEP_INTO, if no more bytes in the outer SEQUENCE or if the tag
|
||
|
// isn't found, pbData and cbData are set to 0. Additioanlly, for
|
||
|
// OPTIONAL_STEP_INTO, all subsequent values are skipped and their
|
||
|
// rgValueBlob[] entries zeroed until a STEP_OUT is encountered.
|
||
|
//
|
||
|
// If MINASN1_RETURN_VALUE_BLOB_FLAG is set, pbData points to
|
||
|
// the tag. cbData includes the tag, length and content octets.
|
||
|
//
|
||
|
// If MINASN1_RETURN_CONTENT_BLOB_FLAG is set, pbData points to the content
|
||
|
// octets. cbData includes only the content octets.
|
||
|
//
|
||
|
// If neither BLOB_FLAG is set, rgValueBlob[] isn't updated.
|
||
|
//
|
||
|
// For MINASN1_RETURN_CONTENT_BLOB_FLAG of a BITSTRING, pbData is
|
||
|
// advanced past the first contents octet containing the number of
|
||
|
// unused bits and cbData has been decremented by 1. If cbData > 0, then,
|
||
|
// *(pbData - 1) will contain the number of unused bits.
|
||
|
//--------------------------------------------------------------------------
|
||
|
LONG
|
||
|
WINAPI
|
||
|
MinAsn1ExtractValues(
|
||
|
IN const BYTE *pbEncoded,
|
||
|
IN DWORD cbEncoded,
|
||
|
IN OUT DWORD *pcValuePara,
|
||
|
IN const MINASN1_EXTRACT_VALUE_PARA *rgValuePara,
|
||
|
IN DWORD cValueBlob,
|
||
|
OUT OPTIONAL PCRYPT_DER_BLOB rgValueBlob
|
||
|
)
|
||
|
{
|
||
|
DWORD cValue = *pcValuePara;
|
||
|
const BYTE *pb = pbEncoded;
|
||
|
DWORD cb = cbEncoded;
|
||
|
BOOL fSkipIntoValues = FALSE;
|
||
|
|
||
|
DWORD iValue;
|
||
|
LONG lAllValues;
|
||
|
|
||
|
STEP_INTO_STACK_ENTRY rgStepIntoStack[MAX_STEP_INTO_DEPTH];
|
||
|
DWORD dwStepIntoDepth = 0;
|
||
|
|
||
|
for (iValue = 0; iValue < cValue; iValue++) {
|
||
|
DWORD dwParaFlags = rgValuePara[iValue].dwFlags;
|
||
|
DWORD dwOp = dwParaFlags & MINASN1_MASK_VALUE_OP;
|
||
|
const BYTE *pbParaTag = rgValuePara[iValue].rgbTag;
|
||
|
DWORD dwIndex = rgValuePara[iValue].dwIndex;
|
||
|
BOOL fValueBlob = (dwParaFlags & (MINASN1_RETURN_VALUE_BLOB_FLAG |
|
||
|
MINASN1_RETURN_CONTENT_BLOB_FLAG)) && rgValueBlob &&
|
||
|
(dwIndex < cValueBlob);
|
||
|
BOOL fSkipValue = FALSE;
|
||
|
|
||
|
LONG lTagLength;
|
||
|
DWORD cbContent;
|
||
|
const BYTE *pbContent;
|
||
|
DWORD cbValue;
|
||
|
|
||
|
if (MINASN1_STEP_OUT_VALUE_OP == dwOp) {
|
||
|
// Unstack and advance past the last STEP_INTO
|
||
|
|
||
|
if (0 == dwStepIntoDepth)
|
||
|
goto InvalidStepOutOp;
|
||
|
|
||
|
dwStepIntoDepth--;
|
||
|
pb = rgStepIntoStack[dwStepIntoDepth].pb;
|
||
|
cb = rgStepIntoStack[dwStepIntoDepth].cb;
|
||
|
fSkipIntoValues = rgStepIntoStack[dwStepIntoDepth].fSkipIntoValues;
|
||
|
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
if (fSkipIntoValues) {
|
||
|
// For an omitted OPTIONAL_STEP_INTO, all of its included values
|
||
|
// are also omitted.
|
||
|
fSkipValue = TRUE;
|
||
|
} else if (0 == cb) {
|
||
|
if (!(MINASN1_OPTIONAL_STEP_INTO_VALUE_OP == dwOp ||
|
||
|
MINASN1_OPTIONAL_STEP_OVER_VALUE_OP == dwOp))
|
||
|
goto TooLittleData;
|
||
|
fSkipValue = TRUE;
|
||
|
} else if (pbParaTag) {
|
||
|
// Assumption: single byte tag for doing comparison
|
||
|
|
||
|
// Check if the encoded tag matches one of the expected tags
|
||
|
|
||
|
BYTE bEncodedTag;
|
||
|
BYTE bParaTag;
|
||
|
|
||
|
bEncodedTag = *pb;
|
||
|
while ((bParaTag = *pbParaTag) && bParaTag != bEncodedTag)
|
||
|
pbParaTag++;
|
||
|
|
||
|
if (0 == bParaTag) {
|
||
|
if (!(MINASN1_OPTIONAL_STEP_INTO_VALUE_OP == dwOp ||
|
||
|
MINASN1_OPTIONAL_STEP_OVER_VALUE_OP == dwOp))
|
||
|
goto InvalidTag;
|
||
|
fSkipValue = TRUE;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if (fSkipValue) {
|
||
|
if (fValueBlob) {
|
||
|
rgValueBlob[dwIndex].pbData = NULL;
|
||
|
rgValueBlob[dwIndex].cbData = 0;
|
||
|
}
|
||
|
|
||
|
if (MINASN1_STEP_INTO_VALUE_OP == dwOp ||
|
||
|
MINASN1_OPTIONAL_STEP_INTO_VALUE_OP == dwOp) {
|
||
|
// Stack this skipped STEP_INTO
|
||
|
if (MAX_STEP_INTO_DEPTH <= dwStepIntoDepth)
|
||
|
goto ExceededStepIntoDepth;
|
||
|
rgStepIntoStack[dwStepIntoDepth].pb = pb;
|
||
|
rgStepIntoStack[dwStepIntoDepth].cb = cb;
|
||
|
rgStepIntoStack[dwStepIntoDepth].fSkipIntoValues =
|
||
|
fSkipIntoValues;
|
||
|
dwStepIntoDepth++;
|
||
|
|
||
|
fSkipIntoValues = TRUE;
|
||
|
}
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
lTagLength = MinAsn1ExtractContent(
|
||
|
pb,
|
||
|
cb,
|
||
|
&cbContent,
|
||
|
&pbContent
|
||
|
);
|
||
|
if (0 >= lTagLength)
|
||
|
goto InvalidTagOrLength;
|
||
|
|
||
|
cbValue = cbContent + lTagLength;
|
||
|
|
||
|
if (fValueBlob) {
|
||
|
if (dwParaFlags & MINASN1_RETURN_CONTENT_BLOB_FLAG) {
|
||
|
rgValueBlob[dwIndex].pbData = (BYTE *) pbContent;
|
||
|
rgValueBlob[dwIndex].cbData = cbContent;
|
||
|
|
||
|
if (MINASN1_TAG_BITSTRING == *pb) {
|
||
|
if (0 < cbContent) {
|
||
|
// Advance past the first contents octet containing
|
||
|
// the number of unused bits
|
||
|
rgValueBlob[dwIndex].pbData += 1;
|
||
|
rgValueBlob[dwIndex].cbData -= 1;
|
||
|
}
|
||
|
}
|
||
|
} else if (dwParaFlags & MINASN1_RETURN_VALUE_BLOB_FLAG) {
|
||
|
rgValueBlob[dwIndex].pbData = (BYTE *) pb;
|
||
|
rgValueBlob[dwIndex].cbData = cbValue;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
switch (dwOp) {
|
||
|
case MINASN1_STEP_INTO_VALUE_OP:
|
||
|
case MINASN1_OPTIONAL_STEP_INTO_VALUE_OP:
|
||
|
// Stack this STEP_INTO
|
||
|
if (MAX_STEP_INTO_DEPTH <= dwStepIntoDepth)
|
||
|
goto ExceededStepIntoDepth;
|
||
|
rgStepIntoStack[dwStepIntoDepth].pb = pb + cbValue;
|
||
|
rgStepIntoStack[dwStepIntoDepth].cb = cb - cbValue;
|
||
|
assert(!fSkipIntoValues);
|
||
|
rgStepIntoStack[dwStepIntoDepth].fSkipIntoValues = FALSE;
|
||
|
dwStepIntoDepth++;
|
||
|
pb = pbContent;
|
||
|
cb = cbContent;
|
||
|
break;
|
||
|
case MINASN1_STEP_OVER_VALUE_OP:
|
||
|
case MINASN1_OPTIONAL_STEP_OVER_VALUE_OP:
|
||
|
pb += cbValue;
|
||
|
cb -= cbValue;
|
||
|
break;
|
||
|
default:
|
||
|
goto InvalidArg;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
lAllValues = (LONG)(pb - pbEncoded);
|
||
|
assert((DWORD) lAllValues <= cbEncoded);
|
||
|
|
||
|
CommonReturn:
|
||
|
*pcValuePara = iValue;
|
||
|
return lAllValues;
|
||
|
|
||
|
InvalidStepOutOp:
|
||
|
TooLittleData:
|
||
|
InvalidTag:
|
||
|
ExceededStepIntoDepth:
|
||
|
InvalidTagOrLength:
|
||
|
InvalidArg:
|
||
|
lAllValues = -((LONG)(pb - pbEncoded)) - 1;
|
||
|
goto CommonReturn;
|
||
|
}
|
||
|
|
||
|
|
||
|
//+-------------------------------------------------------------------------
|
||
|
// Find an extension identified by its Encoded Object Identifier.
|
||
|
//
|
||
|
// Searches the list of parsed extensions returned by
|
||
|
// MinAsn1ParseExtensions().
|
||
|
//
|
||
|
// If found, returns pointer to the rgExtBlob[MINASN1_EXT_BLOB_CNT].
|
||
|
// Otherwise, returns NULL.
|
||
|
//--------------------------------------------------------------------------
|
||
|
PCRYPT_DER_BLOB
|
||
|
WINAPI
|
||
|
MinAsn1FindExtension(
|
||
|
IN PCRYPT_DER_BLOB pEncodedOIDBlob,
|
||
|
IN DWORD cExt,
|
||
|
IN CRYPT_DER_BLOB rgrgExtBlob[][MINASN1_EXT_BLOB_CNT]
|
||
|
)
|
||
|
{
|
||
|
DWORD i;
|
||
|
DWORD cbOID = pEncodedOIDBlob->cbData;
|
||
|
const BYTE *pbOID = pEncodedOIDBlob->pbData;
|
||
|
|
||
|
for (i = 0; i < cExt; i++) {
|
||
|
if (cbOID == rgrgExtBlob[i][MINASN1_EXT_OID_IDX].cbData
|
||
|
&&
|
||
|
0 == memcmp(pbOID, rgrgExtBlob[i][MINASN1_EXT_OID_IDX].pbData,
|
||
|
cbOID))
|
||
|
return rgrgExtBlob[i];
|
||
|
}
|
||
|
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
|
||
|
//+-------------------------------------------------------------------------
|
||
|
// Find the first attribute identified by its Encoded Object Identifier.
|
||
|
//
|
||
|
// Searches the list of parsed attributes returned by
|
||
|
// MinAsn1ParseAttributes().
|
||
|
//
|
||
|
// If found, returns pointer to the rgAttrBlob[MINASN1_ATTR_BLOB_CNT].
|
||
|
// Otherwise, returns NULL.
|
||
|
//--------------------------------------------------------------------------
|
||
|
PCRYPT_DER_BLOB
|
||
|
WINAPI
|
||
|
MinAsn1FindAttribute(
|
||
|
IN PCRYPT_DER_BLOB pEncodedOIDBlob,
|
||
|
IN DWORD cAttr,
|
||
|
IN CRYPT_DER_BLOB rgrgAttrBlob[][MINASN1_ATTR_BLOB_CNT]
|
||
|
)
|
||
|
{
|
||
|
DWORD i;
|
||
|
DWORD cbOID = pEncodedOIDBlob->cbData;
|
||
|
const BYTE *pbOID = pEncodedOIDBlob->pbData;
|
||
|
|
||
|
for (i = 0; i < cAttr; i++) {
|
||
|
if (cbOID == rgrgAttrBlob[i][MINASN1_ATTR_OID_IDX].cbData
|
||
|
&&
|
||
|
0 == memcmp(pbOID, rgrgAttrBlob[i][MINASN1_ATTR_OID_IDX].pbData,
|
||
|
cbOID))
|
||
|
return rgrgAttrBlob[i];
|
||
|
}
|
||
|
|
||
|
return NULL;
|
||
|
}
|
||
|
|
||
|
//+-------------------------------------------------------------------------
|
||
|
// Parses an ASN.1 encoded PKCS #7 Signed Data Message to extract and
|
||
|
// parse the X.509 certificates it contains.
|
||
|
//
|
||
|
// Assumes the PKCS #7 message is definite length encoded.
|
||
|
// Assumes PKCS #7 version 1.5, ie, not the newer CMS version.
|
||
|
//
|
||
|
// Upon input, *pcCert contains the maximum number of parsed certificates
|
||
|
// that can be returned. Updated with the number of certificates processed.
|
||
|
//
|
||
|
// If the encoded message was successfully parsed, TRUE is returned
|
||
|
// with *pcCert updated with the number of parsed certificates. Otherwise,
|
||
|
// FALSE is returned for a parse error.
|
||
|
// Returns:
|
||
|
// success - >= 0 => bytes skipped, length of the encoded certificates
|
||
|
// processed.
|
||
|
// failure - < 0 => negative (offset + 1) of first bad tagged value
|
||
|
// from beginning of message.
|
||
|
//
|
||
|
// The rgrgCertBlob[][] is updated with pointer to and length of the
|
||
|
// fields in the encoded certificate. See MinAsn1ParseCertificate for the
|
||
|
// field definitions.
|
||
|
//--------------------------------------------------------------------------
|
||
|
LONG
|
||
|
WINAPI
|
||
|
MinAsn1ExtractParsedCertificatesFromSignedData(
|
||
|
IN const BYTE *pbEncoded,
|
||
|
IN DWORD cbEncoded,
|
||
|
IN OUT DWORD *pcCert,
|
||
|
OUT CRYPT_DER_BLOB rgrgCertBlob[][MINASN1_CERT_BLOB_CNT]
|
||
|
)
|
||
|
{
|
||
|
LONG lSkipped;
|
||
|
CRYPT_DER_BLOB rgSignedDataBlob[MINASN1_SIGNED_DATA_BLOB_CNT];
|
||
|
|
||
|
lSkipped = MinAsn1ParseSignedData(
|
||
|
pbEncoded,
|
||
|
cbEncoded,
|
||
|
rgSignedDataBlob
|
||
|
);
|
||
|
if (0 >= lSkipped)
|
||
|
goto ParseError;
|
||
|
|
||
|
lSkipped = MinAsn1ParseSignedDataCertificates(
|
||
|
&rgSignedDataBlob[MINASN1_SIGNED_DATA_CERTS_IDX],
|
||
|
pcCert,
|
||
|
rgrgCertBlob
|
||
|
);
|
||
|
|
||
|
if (0 > lSkipped) {
|
||
|
assert(rgSignedDataBlob[MINASN1_SIGNED_DATA_CERTS_IDX].pbData >
|
||
|
pbEncoded);
|
||
|
lSkipped -= rgSignedDataBlob[MINASN1_SIGNED_DATA_CERTS_IDX].pbData -
|
||
|
pbEncoded;
|
||
|
|
||
|
goto ParseError;
|
||
|
}
|
||
|
|
||
|
CommonReturn:
|
||
|
return lSkipped;
|
||
|
|
||
|
ParseError:
|
||
|
*pcCert = 0;
|
||
|
goto CommonReturn;
|
||
|
}
|