|
@@ -218,6 +218,88 @@ extern int wc_InitRsaHw(RsaKey* key);
|
|
|
#endif /* HAVE_SELFTEST */
|
|
|
#endif
|
|
|
|
|
|
+#if defined(WOLFSSL_ASN_PRINT) || defined(WOLFSSL_DEBUG_ASN_TEMPLATE)
|
|
|
+
|
|
|
+/* String representations of tags. */
|
|
|
+static const char* tagString[4][32] = {
|
|
|
+ /* Universal */
|
|
|
+ {
|
|
|
+ "EOC",
|
|
|
+ "BOOLEAN",
|
|
|
+ "INTEGER",
|
|
|
+ "BIT STRING",
|
|
|
+ "OCTET STRING",
|
|
|
+ "NULL",
|
|
|
+ "OBJECT ID",
|
|
|
+ "ObjectDescriptor",
|
|
|
+ "INSTANCE OF",
|
|
|
+ "REAL",
|
|
|
+ "ENUMERATED",
|
|
|
+ "EMBEDDED PDV",
|
|
|
+ "UT8String",
|
|
|
+ "RELATIVE-OID",
|
|
|
+ "(0x0e) 14",
|
|
|
+ "(0x0f) 15",
|
|
|
+ "SEQUENCE",
|
|
|
+ "SET",
|
|
|
+ "NumericString",
|
|
|
+ "PrintableString",
|
|
|
+ "T61String",
|
|
|
+ "VideotexString",
|
|
|
+ "IA5String",
|
|
|
+ "UTCTime",
|
|
|
+ "GeneralizedTime",
|
|
|
+ "GraphicString",
|
|
|
+ "ISO646String",
|
|
|
+ "GeneralString",
|
|
|
+ "UniversalString",
|
|
|
+ "CHARACTER STRING",
|
|
|
+ "BMPString",
|
|
|
+ "(0x1f) 31",
|
|
|
+ },
|
|
|
+ /* Application */
|
|
|
+ {
|
|
|
+ "[A 0]", "[A 1]", "[A 2]", "[A 3]",
|
|
|
+ "[A 4]", "[A 5]", "[A 6]", "[A 7]",
|
|
|
+ "[A 8]", "[A 9]", "[A 10]", "[A 11]",
|
|
|
+ "[A 12]", "[A 13]", "[A 14]", "[A 15]",
|
|
|
+ "[A 16]", "[A 17]", "[A 18]", "[A 19]",
|
|
|
+ "[A 20]", "[A 21]", "[A 22]", "[A 23]",
|
|
|
+ "[A 24]", "[A 25]", "[A 26]", "[A 27]",
|
|
|
+ "[A 28]", "[A 20]", "[A 30]", "[A 31]"
|
|
|
+ },
|
|
|
+ /* Context-Specific */
|
|
|
+ {
|
|
|
+ "[0]", "[1]", "[2]", "[3]", "[4]", "[5]", "[6]", "[7]",
|
|
|
+ "[8]", "[9]", "[10]", "[11]", "[12]", "[13]", "[14]", "[15]",
|
|
|
+ "[16]", "[17]", "[18]", "[19]", "[20]", "[21]", "[22]", "[23]",
|
|
|
+ "[24]", "[25]", "[26]", "[27]", "[28]", "[20]", "[30]", "[31]"
|
|
|
+ },
|
|
|
+ /* Private */
|
|
|
+ {
|
|
|
+ "[P 0]", "[P 1]", "[P 2]", "[P 3]",
|
|
|
+ "[P 4]", "[P 5]", "[P 6]", "[P 7]",
|
|
|
+ "[P 8]", "[P 9]", "[P 10]", "[P 11]",
|
|
|
+ "[P 12]", "[P 13]", "[P 14]", "[P 15]",
|
|
|
+ "[P 16]", "[P 17]", "[P 18]", "[P 19]",
|
|
|
+ "[P 20]", "[P 21]", "[P 22]", "[P 23]",
|
|
|
+ "[P 24]", "[P 25]", "[P 26]", "[P 27]",
|
|
|
+ "[P 28]", "[P 20]", "[P 30]", "[P 31]"
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+/* Converts a tag byte to string.
|
|
|
+ *
|
|
|
+ * @param [in] tag BER tag value to interpret.
|
|
|
+ * @return String corresponding to tag.
|
|
|
+ */
|
|
|
+static const char* TagString(byte tag)
|
|
|
+{
|
|
|
+ return tagString[tag >> 6][tag & ASN_TYPE_MASK];
|
|
|
+}
|
|
|
+
|
|
|
+#endif
|
|
|
+
|
|
|
|
|
|
/* Calculates the minimum number of bytes required to encode the value.
|
|
|
*
|
|
@@ -482,83 +564,6 @@ static word32 SizeASNLength(word32 length)
|
|
|
#endif
|
|
|
|
|
|
#ifdef WOLFSSL_DEBUG_ASN_TEMPLATE
|
|
|
-/* String representations of tags. */
|
|
|
-static const char* tagString[4][32] = {
|
|
|
- /* Universal */
|
|
|
- {
|
|
|
- "EOC",
|
|
|
- "BOOLEAN",
|
|
|
- "INTEGER",
|
|
|
- "BIT STRING",
|
|
|
- "OCTET STRING",
|
|
|
- "NULL",
|
|
|
- "OBJECT ID",
|
|
|
- "ObjectDescriptor",
|
|
|
- "INSTANCE OF",
|
|
|
- "REAL",
|
|
|
- "ENUMERATED",
|
|
|
- "EMBEDDED PDV",
|
|
|
- "UT8String",
|
|
|
- "RELATIVE-OID",
|
|
|
- "(0x0e) 14",
|
|
|
- "(0x0f) 15",
|
|
|
- "SEQUENCE",
|
|
|
- "SET",
|
|
|
- "NumericString",
|
|
|
- "PrintableString",
|
|
|
- "T61String",
|
|
|
- "VideotexString",
|
|
|
- "IA5String",
|
|
|
- "UTCTime",
|
|
|
- "GeneralizedTime",
|
|
|
- "GraphicString",
|
|
|
- "ISO646String",
|
|
|
- "GeneralString",
|
|
|
- "UniversalString",
|
|
|
- "CHARACTER STRING",
|
|
|
- "BMPString",
|
|
|
- "(0x1f) 31",
|
|
|
- },
|
|
|
- /* Application */
|
|
|
- {
|
|
|
- "[A 0]", "[A 1]", "[A 2]", "[A 3]",
|
|
|
- "[A 4]", "[A 5]", "[A 6]", "[A 7]",
|
|
|
- "[A 8]", "[A 9]", "[A 10]", "[A 11]",
|
|
|
- "[A 12]", "[A 13]", "[A 14]", "[A 15]",
|
|
|
- "[A 16]", "[A 17]", "[A 18]", "[A 19]",
|
|
|
- "[A 20]", "[A 21]", "[A 22]", "[A 23]",
|
|
|
- "[A 24]", "[A 25]", "[A 26]", "[A 27]",
|
|
|
- "[A 28]", "[A 20]", "[A 30]", "[A 31]"
|
|
|
- },
|
|
|
- /* Context-Specific */
|
|
|
- {
|
|
|
- "[0]", "[1]", "[2]", "[3]", "[4]", "[5]", "[6]", "[7]",
|
|
|
- "[8]", "[9]", "[10]", "[11]", "[12]", "[13]", "[14]", "[15]",
|
|
|
- "[16]", "[17]", "[18]", "[19]", "[20]", "[21]", "[22]", "[23]",
|
|
|
- "[24]", "[25]", "[26]", "[27]", "[28]", "[20]", "[30]", "[31]"
|
|
|
- },
|
|
|
- /* Private */
|
|
|
- {
|
|
|
- "[P 0]", "[P 1]", "[P 2]", "[P 3]",
|
|
|
- "[P 4]", "[P 5]", "[P 6]", "[P 7]",
|
|
|
- "[P 8]", "[P 9]", "[P 10]", "[P 11]",
|
|
|
- "[P 12]", "[P 13]", "[P 14]", "[P 15]",
|
|
|
- "[P 16]", "[P 17]", "[P 18]", "[P 19]",
|
|
|
- "[P 20]", "[P 21]", "[P 22]", "[P 23]",
|
|
|
- "[P 24]", "[P 25]", "[P 26]", "[P 27]",
|
|
|
- "[P 28]", "[P 20]", "[P 30]", "[P 31]"
|
|
|
- }
|
|
|
-};
|
|
|
-
|
|
|
-/* Converts a tag byte to string.
|
|
|
- *
|
|
|
- * @param [in] tag BER tag value to interpret.
|
|
|
- * @return String corresponding to tag.
|
|
|
- */
|
|
|
-static const char* TagString(byte tag)
|
|
|
-{
|
|
|
- return tagString[tag >> 6][tag & ASN_TYPE_MASK];
|
|
|
-}
|
|
|
|
|
|
#include <stdarg.h>
|
|
|
|
|
@@ -5507,7 +5512,7 @@ int EncodeObjectId(const word16* in, word32 inSz, byte* out, word32* outSz)
|
|
|
}
|
|
|
#endif /* HAVE_OID_ENCODING */
|
|
|
|
|
|
-#ifdef HAVE_OID_DECODING
|
|
|
+#if defined(HAVE_OID_DECODING) || defined(WOLFSSL_ASN_PRINT)
|
|
|
/* Encode dotted form of OID into byte array version.
|
|
|
*
|
|
|
* @param [in] in Byte array containing OID.
|
|
@@ -5537,12 +5542,12 @@ int DecodeObjectId(const byte* in, word32 inSz, word16* out, word32* outSz)
|
|
|
return BUFFER_E;
|
|
|
}
|
|
|
if (y == 0) {
|
|
|
- out[0] = (t / 40);
|
|
|
- out[1] = (t % 40);
|
|
|
+ out[0] = (word16)(t / 40);
|
|
|
+ out[1] = (word16)(t % 40);
|
|
|
y = 2;
|
|
|
}
|
|
|
else {
|
|
|
- out[y++] = t;
|
|
|
+ out[y++] = (word16)t;
|
|
|
}
|
|
|
t = 0; /* reset tmp */
|
|
|
}
|
|
@@ -37020,6 +37025,803 @@ int wc_MIME_free_hdrs(MimeHdr* head)
|
|
|
|
|
|
#undef ERROR_OUT
|
|
|
|
|
|
+
|
|
|
+#ifdef WOLFSSL_ASN_PRINT
|
|
|
+
|
|
|
+/*******************************************************************************
|
|
|
+ * ASN.1 Parsing and Printing Implemenation
|
|
|
+ ******************************************************************************/
|
|
|
+
|
|
|
+/* Initialize ASN.1 print options.
|
|
|
+ *
|
|
|
+ * @param [in, out] opts ASN.1 options for printing.
|
|
|
+ * @return 0 on success.
|
|
|
+ * @return BAD_FUNC_ARG when asn1 is NULL.
|
|
|
+ */
|
|
|
+int wc_Asn1PrintOptions_Init(Asn1PrintOptions* opts)
|
|
|
+{
|
|
|
+ int ret = 0;
|
|
|
+
|
|
|
+ if (opts == NULL) {
|
|
|
+ ret = BAD_FUNC_ARG;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ XMEMSET(opts, 0, sizeof(*opts));
|
|
|
+ }
|
|
|
+
|
|
|
+ return ret;
|
|
|
+}
|
|
|
+
|
|
|
+/* Set a print option into Asn1PrintOptions object.
|
|
|
+ *
|
|
|
+ * @param [in, out] opts ASN.1 options for printing.
|
|
|
+ * @param [in] opt Option to set value of.
|
|
|
+ * @param [in] val Value to set for option.
|
|
|
+ * @return 0 on success.
|
|
|
+ * @return BAD_FUNC_ARG when asn1 is NULL.
|
|
|
+ * @return BAD_FUNC_ARG when val is out of range for option.
|
|
|
+ */
|
|
|
+int wc_Asn1PrintOptions_Set(Asn1PrintOptions* opts, enum Asn1PrintOpt opt,
|
|
|
+ word32 val)
|
|
|
+{
|
|
|
+ int ret = 0;
|
|
|
+
|
|
|
+ /* Validate parameters. */
|
|
|
+ if (opts == NULL) {
|
|
|
+ ret = BAD_FUNC_ARG;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ret == 0) {
|
|
|
+ switch (opt) {
|
|
|
+ /* Offset into DER/BER data to start decoding from. */
|
|
|
+ case ASN1_PRINT_OPT_OFFSET:
|
|
|
+ opts->offset = val;
|
|
|
+ break;
|
|
|
+ /* Length of DER/BER encoding to parse. */
|
|
|
+ case ASN1_PRINT_OPT_LENGTH:
|
|
|
+ opts->length = val;
|
|
|
+ break;
|
|
|
+ /* Number of spaces to indent for each change in depth. */
|
|
|
+ case ASN1_PRINT_OPT_INDENT:
|
|
|
+ /* Only 4 bits available for value. */
|
|
|
+ if (val >= (1 << 4)) {
|
|
|
+ ret = BAD_FUNC_ARG;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ opts->indent = val;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ /* Draw branches instead of indenting. */
|
|
|
+ case ASN1_PRINT_OPT_DRAW_BRANCH:
|
|
|
+ /* Boolean value. */
|
|
|
+ if (val > 1) {
|
|
|
+ ret = BAD_FUNC_ARG;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ opts->draw_branch = val;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ /* Show raw data of primitive types as octets. */
|
|
|
+ case ASN1_PRINT_OPT_SHOW_DATA:
|
|
|
+ /* Boolean value. */
|
|
|
+ if (val > 1) {
|
|
|
+ ret = BAD_FUNC_ARG;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ opts->show_data = val;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ /* Show header data as octets. */
|
|
|
+ case ASN1_PRINT_OPT_SHOW_HEADER_DATA:
|
|
|
+ /* Boolean value. */
|
|
|
+ if (val > 1) {
|
|
|
+ ret = BAD_FUNC_ARG;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ opts->show_header_data = val;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ /* Show the wolfSSL OID value for OBJECT_ID. */
|
|
|
+ case ASN1_PRINT_OPT_SHOW_OID:
|
|
|
+ /* Boolean value. */
|
|
|
+ if (val > 1) {
|
|
|
+ ret = BAD_FUNC_ARG;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ opts->show_oid = val;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ /* Don't show text representations of primitive types. */
|
|
|
+ case ASN1_PRINT_OPT_SHOW_NO_TEXT:
|
|
|
+ /* Boolean value. */
|
|
|
+ if (val > 1) {
|
|
|
+ ret = BAD_FUNC_ARG;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ opts->show_no_text = val;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ /* Don't show dump text representations of primitive types. */
|
|
|
+ case ASN1_PRINT_OPT_SHOW_NO_DUMP_TEXT:
|
|
|
+ /* Boolean value. */
|
|
|
+ if (val > 1) {
|
|
|
+ ret = BAD_FUNC_ARG;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ opts->show_no_dump_text = val;
|
|
|
+ }
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return ret;
|
|
|
+}
|
|
|
+
|
|
|
+/* Initialize an ASN.1 parse object.
|
|
|
+ *
|
|
|
+ * @param [in, out] asn1 ASN.1 parse object.
|
|
|
+ * @return 0 on success.
|
|
|
+ * @return BAD_FUNC_ARG when asn1 is NULL.
|
|
|
+ */
|
|
|
+int wc_Asn1_Init(Asn1* asn1)
|
|
|
+{
|
|
|
+ int ret = 0;
|
|
|
+
|
|
|
+ if (asn1 == NULL) {
|
|
|
+ ret = BAD_FUNC_ARG;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ XMEMSET(asn1, 0, sizeof(*asn1));
|
|
|
+ asn1->file = XBADFILE;
|
|
|
+ }
|
|
|
+
|
|
|
+ return ret;
|
|
|
+}
|
|
|
+
|
|
|
+/* Set the file to use when printing.
|
|
|
+ *
|
|
|
+ * @param [in, out] asn1 ASN.1 parse object.
|
|
|
+ * @param [in] file File to print to.
|
|
|
+ * @return 0 on success.
|
|
|
+ * @return BAD_FUNC_ARG when asn1 is NULL.
|
|
|
+ * @return BAD_FUNC_ARG when file is XBADFILE.
|
|
|
+ */
|
|
|
+int wc_Asn1_SetFile(Asn1* asn1, XFILE file)
|
|
|
+{
|
|
|
+ int ret = 0;
|
|
|
+
|
|
|
+ if ((asn1 == NULL) || (file == XBADFILE)) {
|
|
|
+ ret = BAD_FUNC_ARG;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ asn1->file = file;
|
|
|
+ }
|
|
|
+
|
|
|
+ return ret;
|
|
|
+}
|
|
|
+
|
|
|
+/* Maximum OID dotted form size. */
|
|
|
+#define ASN1_OID_DOTTED_MAX_SZ 16
|
|
|
+
|
|
|
+/* Print OID in dotted form or as hex bytes.
|
|
|
+ *
|
|
|
+ * @param [in] file File pointer to write to.
|
|
|
+ * @param [in] oid OBJECT_ID data.
|
|
|
+ * @param [in] oid_len Length of OBJECT_ID data.
|
|
|
+ */
|
|
|
+static void PrintObjectIdNum(XFILE file, unsigned char* oid, word32 len)
|
|
|
+{
|
|
|
+ word16 dotted_nums[ASN1_OID_DOTTED_MAX_SZ];
|
|
|
+ word32 num = ASN1_OID_DOTTED_MAX_SZ;
|
|
|
+ word32 i;
|
|
|
+
|
|
|
+ /* Decode OBJECT_ID into dotted form array. */
|
|
|
+ if (DecodeObjectId(oid, len, dotted_nums, &num) == 0) {
|
|
|
+ /* Print out each number of dotted form. */
|
|
|
+ for (i = 0; i < num; i++) {
|
|
|
+ XFPRINTF(file, "%d", dotted_nums[i]);
|
|
|
+ /* Add separetor. */
|
|
|
+ if (i < num - 1) {
|
|
|
+ XFPRINTF(file, ".");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ /* Print out bytes as we couldn't decode. */
|
|
|
+ for (i = 0; i < len; i++) {
|
|
|
+ XFPRINTF(file, "%02x", oid[i]);
|
|
|
+ /* Add separetor. */
|
|
|
+ if (i < len - 1) {
|
|
|
+ XFPRINTF(file, ":");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* OID value to name mapping. */
|
|
|
+typedef struct OidName {
|
|
|
+ /* wolfSSL OID value. */
|
|
|
+ word32 oid;
|
|
|
+ /* Long name to print when OID seen. */
|
|
|
+ const char* name;
|
|
|
+} OidName;
|
|
|
+
|
|
|
+/* Extra OID to name mappings. */
|
|
|
+static const OidName extraOids[] = {
|
|
|
+ { 0x005c, "commonName" },
|
|
|
+ { 0x005d, "surname" },
|
|
|
+ { 0x005e, "serialNumber" },
|
|
|
+ { 0x005f, "countryName" },
|
|
|
+ { 0x0060, "localityName" },
|
|
|
+ { 0x0061, "stateOrProvinceName" },
|
|
|
+ { 0x0062, "streetAddress" },
|
|
|
+ { 0x0063, "organizationName" },
|
|
|
+ { 0x0064, "organizationUnitName" },
|
|
|
+ { 0x0065, "title" },
|
|
|
+ { 0x0086, "certificateExtension" },
|
|
|
+ { 0x028d, "emailAddress" },
|
|
|
+ { 0x0293, "challengePassword" },
|
|
|
+ { 0x029a, "extensionReq" },
|
|
|
+};
|
|
|
+/* Length of table of extra OID to name mappings. */
|
|
|
+#define EXTRA_OIDS_LEN ((int)(sizeof(extraOids) / sizeof(*extraOids)))
|
|
|
+
|
|
|
+/* Convert OID value to long name.
|
|
|
+ *
|
|
|
+ * @param [in] oid OID value.
|
|
|
+ * @param [out] name Long name for OID when known.
|
|
|
+ * @return 1 when OID known.
|
|
|
+ * @return 0 when OID not known.
|
|
|
+ */
|
|
|
+static int Oid2LongName(word32 oid, const char** name)
|
|
|
+{
|
|
|
+ int ret = 0;
|
|
|
+ int i;
|
|
|
+
|
|
|
+ /* Step through each entry in table. */
|
|
|
+ for (i = 0; i < EXTRA_OIDS_LEN; i++) {
|
|
|
+ if (extraOids[i].oid == oid) {
|
|
|
+ /* Return the name associated with the OID value. */
|
|
|
+ *name = extraOids[i].name;
|
|
|
+ ret = 1;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return ret;
|
|
|
+}
|
|
|
+
|
|
|
+/* Print the text version of the OBJECT_ID.
|
|
|
+ *
|
|
|
+ * @param [in] asn1 ASN.1 parse object.
|
|
|
+ * @param [in] opts ASN.1 options for printing.
|
|
|
+ */
|
|
|
+static void PrintObjectIdText(Asn1* asn1, Asn1PrintOptions* opts)
|
|
|
+{
|
|
|
+ word32 oid = (word32)-1;
|
|
|
+#if !defined(WOLFCRYPT_ONLY) && defined(OPENSSL_EXTRA)
|
|
|
+ word32 nid;
|
|
|
+#endif
|
|
|
+ const char* ln = NULL;
|
|
|
+ word32 i = 0;
|
|
|
+ int known = 1;
|
|
|
+
|
|
|
+ /* Get the OID value for the OBJECT_ID. */
|
|
|
+ GetObjectId(asn1->data + asn1->offset, &i, &oid, oidIgnoreType,
|
|
|
+ asn1->item.len + 2);
|
|
|
+#if !defined(WOLFCRYPT_ONLY) && defined(OPENSSL_EXTRA)
|
|
|
+ /* Lookup NID for OID value. */
|
|
|
+ if ((nid = oid2nid(oid, oidIgnoreType)) != (word32)-1) {
|
|
|
+ /* Lookup long name for NID. */
|
|
|
+ ln = wolfSSL_OBJ_nid2ln(nid);
|
|
|
+ }
|
|
|
+ else
|
|
|
+#endif
|
|
|
+ /* Lookup long name for extra known OID values. */
|
|
|
+ if (!Oid2LongName(oid, &ln)) {
|
|
|
+ /* Unknown OID value. */
|
|
|
+ ln = NULL;
|
|
|
+ known = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ XFPRINTF(asn1->file, ":");
|
|
|
+ /* Show OID value if not known or asked to. */
|
|
|
+ if ((!known) || opts->show_oid) {
|
|
|
+ XFPRINTF(asn1->file, "(0x%x) ", oid);
|
|
|
+ }
|
|
|
+ if (ln != NULL) {
|
|
|
+ /* Print long name. */
|
|
|
+ XFPRINTF(asn1->file, "%s", ln);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ /* Print out as numbers - either dotted or hex values. */
|
|
|
+ PrintObjectIdNum(asn1->file, asn1->data + asn1->item.data_idx,
|
|
|
+ asn1->item.len);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* Print ASN.1 data as a character string.
|
|
|
+ *
|
|
|
+ * @param [in] asn1 ASN.1 parse object.
|
|
|
+ */
|
|
|
+static void PrintText(Asn1* asn1)
|
|
|
+{
|
|
|
+ word32 i;
|
|
|
+
|
|
|
+ XFPRINTF(asn1->file, ":");
|
|
|
+ /* Print all data bytes as characters. */
|
|
|
+ for (i = 0; i < asn1->item.len; i++) {
|
|
|
+ XFPRINTF(asn1->file, "%c", asn1->data[asn1->item.data_idx + i]);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* Print data as a hex bytes.
|
|
|
+ *
|
|
|
+ * @param [in] file File pointer to write to.
|
|
|
+ * @param [in] data Data to print.
|
|
|
+ * @param [in] len Number of bytes to print.
|
|
|
+ */
|
|
|
+static void PrintHex(XFILE file, unsigned char* data, word32 len)
|
|
|
+{
|
|
|
+ word32 i;
|
|
|
+
|
|
|
+ /* Print data bytes as hex numbers. */
|
|
|
+ for (i = 0; i < len; i++) {
|
|
|
+ XFPRINTF(file, "%02x", data[i]);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* Print ASN.1 data as a hex bytes.
|
|
|
+ *
|
|
|
+ * @param [in] asn1 ASN.1 parse object.
|
|
|
+ */
|
|
|
+static void PrintHexText(Asn1* asn1)
|
|
|
+{
|
|
|
+ XFPRINTF(asn1->file, ":");
|
|
|
+ PrintHex(asn1->file, asn1->data + asn1->item.data_idx, asn1->item.len);
|
|
|
+}
|
|
|
+
|
|
|
+/* Print ASN.1 BIT_STRING data as hex bytes noting special first byte.
|
|
|
+ *
|
|
|
+ * @param [in] asn1 ASN.1 parse object.
|
|
|
+ */
|
|
|
+static void PrintBitStringText(Asn1* asn1)
|
|
|
+{
|
|
|
+ if (asn1->item.len > 0) {
|
|
|
+ XFPRINTF(asn1->file, ":[%02x]", asn1->data[asn1->item.data_idx]);
|
|
|
+ PrintHex(asn1->file, asn1->data + asn1->item.data_idx + 1,
|
|
|
+ asn1->item.len - 1);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* Print ASN.1 BOOLEAN data as text with value.
|
|
|
+ *
|
|
|
+ * @param [in] asn1 ASN.1 parse object.
|
|
|
+ */
|
|
|
+static void PrintBooleanText(Asn1* asn1)
|
|
|
+{
|
|
|
+ /* Booleans should be 1 byte of data. */
|
|
|
+ if (asn1->item.len == 1) {
|
|
|
+ XFPRINTF(asn1->file, ":%s (%d)",
|
|
|
+ (asn1->data[asn1->item.data_idx] == 0) ? "FALSE" : "TRUE",
|
|
|
+ asn1->data[asn1->item.data_idx]);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* Print ASN.1 data as single byte +/- number.
|
|
|
+ *
|
|
|
+ * @param [in] asn1 ASN.1 parse object.
|
|
|
+ */
|
|
|
+static void PrintNumberText(Asn1* asn1)
|
|
|
+{
|
|
|
+ /* Only supporting 1 byte of data for now. */
|
|
|
+ if (asn1->item.len == 1) {
|
|
|
+ int num = asn1->data[asn1->item.data_idx];
|
|
|
+
|
|
|
+ XFPRINTF(asn1->file, ":%d", num >= 0x80 ? num - 0x100 : num);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* Print ASN.1 data as a text based on the tag.
|
|
|
+ *
|
|
|
+ * TODO: handle more tags.
|
|
|
+ *
|
|
|
+ * @param [in] asn1 ASN.1 parse object.
|
|
|
+ * @param [in] opts ASN.1 options for printing.
|
|
|
+ */
|
|
|
+static void PrintAsn1Text(Asn1* asn1, Asn1PrintOptions* opts)
|
|
|
+{
|
|
|
+ /* Get the long name for OBJECT_ID where possible. */
|
|
|
+ if (asn1->item.tag == ASN_OBJECT_ID) {
|
|
|
+ PrintObjectIdText(asn1, opts);
|
|
|
+ }
|
|
|
+ /* Data is an array of printable characters. */
|
|
|
+ else if ((asn1->item.tag == ASN_UTF8STRING) ||
|
|
|
+ (asn1->item.tag == ASN_IA5_STRING) ||
|
|
|
+ (asn1->item.tag == ASN_PRINTABLE_STRING) ||
|
|
|
+ (asn1->item.tag == ASN_T61STRING) ||
|
|
|
+ (asn1->item.tag == ASN_BMPSTRING) ||
|
|
|
+ (asn1->item.tag == ASN_UTC_TIME) ||
|
|
|
+ (asn1->item.tag == ASN_GENERALIZED_TIME) ||
|
|
|
+ (asn1->item.tag == ASN_UNIVERSALSTRING) ||
|
|
|
+ (asn1->item.tag == ASN_OBJECT_DESC) ||
|
|
|
+ (asn1->item.tag == ASN_CHARACTER_STRING)) {
|
|
|
+ PrintText(asn1);
|
|
|
+ }
|
|
|
+ /* Show TRUE and FALSE with number. */
|
|
|
+ else if (asn1->item.tag == ASN_BOOLEAN) {
|
|
|
+ PrintBooleanText(asn1);
|
|
|
+ }
|
|
|
+ /* Show number. */
|
|
|
+ else if (asn1->item.tag == ASN_ENUMERATED) {
|
|
|
+ PrintNumberText(asn1);
|
|
|
+ }
|
|
|
+ /* Dumping potentially long string of hex digites. */
|
|
|
+ else if (!opts->show_no_dump_text) {
|
|
|
+ /* Dump all bytes. */
|
|
|
+ if ((asn1->item.tag == ASN_INTEGER) ||
|
|
|
+ (asn1->item.tag == ASN_OCTET_STRING) ||
|
|
|
+ ((asn1->item.tag > ASN_APPLICATION) && (asn1->item.cons))) {
|
|
|
+ PrintHexText(asn1);
|
|
|
+ }
|
|
|
+ /* First byte is number of unused bits in last byte.
|
|
|
+ * Print first specially and dump rest of the bytes. */
|
|
|
+ else if (asn1->item.tag == ASN_BIT_STRING) {
|
|
|
+ PrintBitStringText(asn1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+#define HexToChar(n) ((((n) >= 32) && ((n) < 127)) ? (n) : '.')
|
|
|
+
|
|
|
+/* Dump data as hex bytes.
|
|
|
+ *
|
|
|
+ * @param [in] file File pointer to write to.
|
|
|
+ * @param [in] data Data to print.
|
|
|
+ * @param [in] len Number of bytes to print.
|
|
|
+ */
|
|
|
+static void DumpData(XFILE file, unsigned char* data, word32 len)
|
|
|
+{
|
|
|
+ word32 i;
|
|
|
+ word32 j;
|
|
|
+
|
|
|
+ for (i = 0; i < len; i += j) {
|
|
|
+ /* Print offset. */
|
|
|
+ XFPRINTF(file, " %04x:", i);
|
|
|
+ for (j = 0; (j < 16) && (i + j < len); j++) {
|
|
|
+ /* Print byte as hex number. */
|
|
|
+ XFPRINTF(file, "%s%02x", (j == 8) ? " " : " ", data[i + j]);
|
|
|
+ }
|
|
|
+ /* Print spaces between hex and characters. */
|
|
|
+ XFPRINTF(file, " %*s", (16 - j) * 3 + ((j < 8) ? 1 : 0), "");
|
|
|
+ for (j = 0; (j < 16) && (i + j < len); j++) {
|
|
|
+ /* Print byte as hex number. */
|
|
|
+ XFPRINTF(file, "%c", HexToChar(data[i + j]));
|
|
|
+ }
|
|
|
+ XFPRINTF(file, "\n");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* Update current depth based on the current position.
|
|
|
+ *
|
|
|
+ * @param [in, out] asn1 ASN.1 parse object.
|
|
|
+ */
|
|
|
+static void UpdateDepth(Asn1* asn1)
|
|
|
+{
|
|
|
+ /* If current index is greater than or equal end index then it is done. */
|
|
|
+ while ((asn1->depth > 0) &&
|
|
|
+ (asn1->end_idx[asn1->depth-1] <= asn1->curr)) {
|
|
|
+ /* Move up a depth. */
|
|
|
+ asn1->depth--;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* Check validity of end index of constructed ASN.1 items.
|
|
|
+ *
|
|
|
+ * @param [in, out] asn1 ASN.1 parse object.
|
|
|
+ * @return 0 on success.
|
|
|
+ * @return ASN_DEPTH_E when end offset invalid.
|
|
|
+ */
|
|
|
+static int CheckDepth(Asn1* asn1)
|
|
|
+{
|
|
|
+ int ret = 0;
|
|
|
+ int i;
|
|
|
+ word32 curr_end = asn1->curr + asn1->item.len;
|
|
|
+
|
|
|
+ for (i = 0; (ret == 0) && (i < asn1->depth); i++) {
|
|
|
+ /* Each end index must be at least as large as the current one. */
|
|
|
+ if (asn1->end_idx[i] < asn1->end_idx[asn1->depth]) {
|
|
|
+ ret = ASN_DEPTH_E;
|
|
|
+ }
|
|
|
+ /* Each end index must be at least as large as current index. */
|
|
|
+ if (asn1->end_idx[i] < curr_end) {
|
|
|
+ ret = ASN_DEPTH_E;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return ret;
|
|
|
+}
|
|
|
+
|
|
|
+/* Draw branching based on depth for an ASN.1 item.
|
|
|
+ *
|
|
|
+ * @param [in] asn1 ASN.1 parse object.
|
|
|
+ */
|
|
|
+static void DrawBranch(Asn1* asn1)
|
|
|
+{
|
|
|
+ int i;
|
|
|
+ word32 end = asn1->curr + asn1->item.len;
|
|
|
+
|
|
|
+ /* Write out the character for all depths but current. */
|
|
|
+ for (i = 0; i < asn1->depth; i++) {
|
|
|
+ if (asn1->item.cons || (end < asn1->end_idx[i])) {
|
|
|
+ if (i < asn1->depth - 1) {
|
|
|
+ /* Constructed or not end index and not current depth: | */
|
|
|
+ XFPRINTF(asn1->file, "\xe2\x94\x82");
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ /* Constructed or not end index and current depth: |- */
|
|
|
+ XFPRINTF(asn1->file, "\xe2\x94\x9c");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else if ((i > 1) && (end >= asn1->end_idx[i-1])) {
|
|
|
+ /* End index for previous: _|_ (in top half) */
|
|
|
+ XFPRINTF(asn1->file, "\xe2\x94\xb4");
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ /* End index but not for previous: L (in top half) */
|
|
|
+ XFPRINTF(asn1->file, "\xe2\x94\x94");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ /* Prefix to tag name. */
|
|
|
+ if (asn1->item.cons) {
|
|
|
+ if (asn1->depth > 0) {
|
|
|
+ /* Have other line to connect to: T (in bottom half) */
|
|
|
+ XFPRINTF(asn1->file, "\xe2\x94\xac");
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ /* Have no other line to connect to: r */
|
|
|
+ XFPRINTF(asn1->file, "\xe2\x94\x8c");
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ /* In a sequence: - */
|
|
|
+ XFPRINTF(asn1->file, "\xe2\x94\x80");
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* Print data as hex bytes separated by space.
|
|
|
+ *
|
|
|
+ * @param [in] file File pointer to write to.
|
|
|
+ * @param [in] data Data to print.
|
|
|
+ * @param [in] len Number of bytes to print.
|
|
|
+ */
|
|
|
+static void PrintHexBytes(XFILE file, unsigned char* data, int len)
|
|
|
+{
|
|
|
+ int i;
|
|
|
+
|
|
|
+ for (i = 0; i < len; i++) {
|
|
|
+ XFPRINTF(file, " %02x", data[i]);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/* Dump header data.
|
|
|
+ *
|
|
|
+ * @param [in] asn1 ASN.1 parse object.
|
|
|
+ * @param [in] opts ASN.1 options for printing.
|
|
|
+ */
|
|
|
+static void DumpHeader(Asn1* asn1, Asn1PrintOptions* opts)
|
|
|
+{
|
|
|
+ /* Put on same line when not showing data too and not showing text data. */
|
|
|
+ if ((!opts->show_data) && opts->show_no_text) {
|
|
|
+ XFPRINTF(asn1->file, "%10s %02x", "", asn1->item.tag);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ /* Align with start of data. */
|
|
|
+ XFPRINTF(asn1->file, "\n%12s %02x", "", asn1->item.tag);
|
|
|
+ }
|
|
|
+ /* Print the header bytes as hex bytes separated by a space. */
|
|
|
+ PrintHexBytes(asn1->file, asn1->data + asn1->offset + 1,
|
|
|
+ asn1->curr - (asn1->offset + 1));
|
|
|
+}
|
|
|
+
|
|
|
+/* Print ASN.1 item info based on header and indeces.
|
|
|
+ *
|
|
|
+ * @param [in] asn1 ASN.1 parse object.
|
|
|
+ * @param [in] opts ASN.1 options for printing.
|
|
|
+ */
|
|
|
+static void PrintInfo(Asn1* asn1, Asn1PrintOptions* opts)
|
|
|
+{
|
|
|
+ /* Print offset of this ASN.1 item. */
|
|
|
+ XFPRINTF(asn1->file, "%4d: ", asn1->offset);
|
|
|
+ /* Print length of header. */
|
|
|
+ XFPRINTF(asn1->file, "%1d ", asn1->curr - asn1->offset);
|
|
|
+ /* Print data length. */
|
|
|
+ XFPRINTF(asn1->file, "%c%4d%c", asn1->item.cons ? '[' : '+', asn1->item.len,
|
|
|
+ asn1->item.cons ? ']' : ' ');
|
|
|
+ /* Print depth. */
|
|
|
+ XFPRINTF(asn1->file, " %s(%d)", (asn1->depth < 10) ? " " : "", asn1->depth);
|
|
|
+ if (!opts->draw_branch) {
|
|
|
+ /* Indent to depth as required. */
|
|
|
+ XFPRINTF(asn1->file, "%*s ", asn1->depth * opts->indent, "");
|
|
|
+ if (!opts->indent) {
|
|
|
+ /* Indicate constructed if no indent. */
|
|
|
+ XFPRINTF(asn1->file, "%c", asn1->item.cons ? '+' : ' ');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ /* Draw branch structure for ASN.1 item. */
|
|
|
+ XFPRINTF(asn1->file, " ");
|
|
|
+ DrawBranch(asn1);
|
|
|
+ }
|
|
|
+ /* Print tag name. */
|
|
|
+ XFPRINTF(asn1->file, "%-16s", TagString(asn1->item.tag));
|
|
|
+}
|
|
|
+
|
|
|
+/* Expecting tag part of ASN.1 item. */
|
|
|
+#define ASN_PART_TAG 0
|
|
|
+/* Expecting length part of ASN.1 item. */
|
|
|
+#define ASN_PART_LENGTH 1
|
|
|
+/* Expecting data part of ASN.1 item. */
|
|
|
+#define ASN_PART_DATA 2
|
|
|
+
|
|
|
+/* Print next ASN.1 item.
|
|
|
+ *
|
|
|
+ * @param [in, out] asn1 ASN.1 parse object.
|
|
|
+ * @param [in] opts ASN.1 print options.
|
|
|
+ * @return 0 on success.
|
|
|
+ * @return BAD_FUNC_ARG when asn1 or opts is NULL.
|
|
|
+ * @return ASN_LEN_E when ASN.1 item's length too long.
|
|
|
+ * @return ASN_DEPTH_E when end offset invalid.
|
|
|
+ */
|
|
|
+static int wc_Asn1_Print(Asn1* asn1, Asn1PrintOptions* opts)
|
|
|
+{
|
|
|
+ int ret = 0;
|
|
|
+
|
|
|
+ if ((asn1 == NULL) || (opts == NULL)) {
|
|
|
+ ret = BAD_FUNC_ARG;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Process tag. */
|
|
|
+ if (asn1->part == ASN_PART_TAG) {
|
|
|
+ /* Recalculate which depth we are at. */
|
|
|
+ UpdateDepth(asn1);
|
|
|
+ /* Get tag. */
|
|
|
+ asn1->item.tag = asn1->data[asn1->curr] & ~ASN_CONSTRUCTED;
|
|
|
+ /* Store whether tag indicates constructed. */
|
|
|
+ asn1->item.cons = (asn1->data[asn1->curr] & ASN_CONSTRUCTED) ==
|
|
|
+ ASN_CONSTRUCTED;
|
|
|
+ /* Start of ASN.1 item is current index. */
|
|
|
+ asn1->offset = asn1->curr;
|
|
|
+ /* Step over tag. */
|
|
|
+ asn1->curr++;
|
|
|
+ /* Next part is length. */
|
|
|
+ asn1->part = ASN_PART_LENGTH;
|
|
|
+ }
|
|
|
+ /* Process length. */
|
|
|
+ if (asn1->part == ASN_PART_LENGTH) {
|
|
|
+ int len;
|
|
|
+
|
|
|
+ /* Decode length and step over it. */
|
|
|
+ if (GetLength(asn1->data, &asn1->curr, &len, asn1->max) < 0) {
|
|
|
+ ret = ASN_LEN_E;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ /* Store ASN.1 item data offset. */
|
|
|
+ asn1->item.data_idx = asn1->curr;
|
|
|
+ /* Store ASN.1 item data length. */
|
|
|
+ asn1->item.len = len;
|
|
|
+
|
|
|
+ /* Print info about ASN.1 item. */
|
|
|
+ PrintInfo(asn1, opts);
|
|
|
+
|
|
|
+ if (!asn1->item.cons) {
|
|
|
+ /* Move on to print data. */
|
|
|
+ asn1->part = ASN_PART_DATA;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ /* Print header now if not printing data. */
|
|
|
+ if (opts->show_header_data) {
|
|
|
+ DumpHeader(asn1, opts);
|
|
|
+ }
|
|
|
+ XFPRINTF(asn1->file, "\n");
|
|
|
+ /* Record end offset for this depth. */
|
|
|
+ asn1->end_idx[asn1->depth++] = asn1->curr + asn1->item.len;
|
|
|
+ /* Done with this ASN.1 item. */
|
|
|
+ asn1->part = ASN_PART_TAG;
|
|
|
+ }
|
|
|
+ /* Check end indeces are valid. */
|
|
|
+ ret = CheckDepth(asn1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ /* Process data. */
|
|
|
+ if ((ret == 0) && (asn1->part == ASN_PART_DATA)) {
|
|
|
+ if (!opts->show_no_text) {
|
|
|
+ /* Print text representation of data. */
|
|
|
+ PrintAsn1Text(asn1, opts);
|
|
|
+ }
|
|
|
+ if (opts->show_header_data) {
|
|
|
+ /* Dump header bytes. */
|
|
|
+ DumpHeader(asn1, opts);
|
|
|
+ }
|
|
|
+ XFPRINTF(asn1->file, "\n");
|
|
|
+ if (opts->show_data) {
|
|
|
+ /* Dump data bytes. */
|
|
|
+ DumpData(asn1->file, asn1->data + asn1->item.data_idx,
|
|
|
+ asn1->item.len);
|
|
|
+ }
|
|
|
+ /* Step past data to next ASN.1 item. */
|
|
|
+ asn1->curr += asn1->item.len;
|
|
|
+ /* Update the depth based on end indeces. */
|
|
|
+ UpdateDepth(asn1);
|
|
|
+ /* Done with this ASN.1 item. */
|
|
|
+ asn1->part = ASN_PART_TAG;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Make ASN.1 item printing go out. */
|
|
|
+ fflush(asn1->file);
|
|
|
+
|
|
|
+ return ret;
|
|
|
+}
|
|
|
+
|
|
|
+/* Print all ASN.1 items.
|
|
|
+ *
|
|
|
+ * @param [in, out] asn1 ASN.1 parse object.
|
|
|
+ * @param [in] opts ASN.1 print options.
|
|
|
+ * @param [in] data BER/DER data to print.
|
|
|
+ * @param [in] len Length of data to print in bytes.
|
|
|
+ * @return 0 on success.
|
|
|
+ * @return BAD_FUNC_ARG when asn1 or opts is NULL.
|
|
|
+ * @return ASN_LEN_E when ASN.1 item's length too long.
|
|
|
+ * @return ASN_DEPTH_E when end offset invalid.
|
|
|
+ * @return ASN_PARSE_E when not all of an ASN.1 item parsed.
|
|
|
+ */
|
|
|
+int wc_Asn1_PrintAll(Asn1* asn1, Asn1PrintOptions* opts, unsigned char* data,
|
|
|
+ word32 len)
|
|
|
+{
|
|
|
+ int ret = 0;
|
|
|
+
|
|
|
+ if (asn1 == NULL) {
|
|
|
+ ret = BAD_FUNC_ARG;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (ret == 0) {
|
|
|
+ /* Initialize start position. */
|
|
|
+ asn1->curr = 0;
|
|
|
+ /* Start parsing at tag. */
|
|
|
+ asn1->part = ASN_PART_TAG;
|
|
|
+ /* Start depth at 0. */
|
|
|
+ asn1->depth = 0;
|
|
|
+
|
|
|
+ /* Store the starting point of the data to parse. */
|
|
|
+ asn1->data = data + opts->offset;
|
|
|
+ if (opts->length > 0) {
|
|
|
+ /* Use user specified maximum length. */
|
|
|
+ asn1->max = opts->length;
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ /* Maximum length is up to end from offset. */
|
|
|
+ asn1->max = len - opts->offset;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* Keep going while no error and have data to parse. */
|
|
|
+ while ((ret == 0) && (asn1->curr < asn1->max)) {
|
|
|
+ /* Print an ASN.1 item. */
|
|
|
+ ret = wc_Asn1_Print(asn1, opts);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if ((ret == 0) && (asn1->part != ASN_PART_TAG)) {
|
|
|
+ /* Stopped before finishing ASN.1 item. */
|
|
|
+ ret = ASN_PARSE_E;
|
|
|
+ }
|
|
|
+ if ((ret == 0) && (asn1->depth != 0)) {
|
|
|
+ /* Stopped without seeing all items in a constructed item. */
|
|
|
+ ret = ASN_DEPTH_E;
|
|
|
+ }
|
|
|
+
|
|
|
+ return ret;
|
|
|
+}
|
|
|
+
|
|
|
+#endif /* WOLFSSL_ASN_PRINT */
|
|
|
#endif /* !NO_ASN */
|
|
|
|
|
|
#ifdef WOLFSSL_SEP
|