From 2249b98d0425e55db643378de18f264704ef5b4c Mon Sep 17 00:00:00 2001 From: Alberto Leiva Popper Date: Fri, 21 Sep 2018 17:42:58 -0500 Subject: [PATCH] About 80% of RFC 6488 --- autogen.sh | 2 +- configure.ac | 9 +- src/Makefile.am | 12 +- src/common.h | 13 +++ src/content_info.c | 115 +++++++++++++++++++ src/content_info.h | 11 ++ src/file.c | 88 +++++++++++++++ src/file.h | 19 ++++ src/line_file.c | 2 +- src/main.c | 28 ++++- src/oid.c | 55 +++++++++ src/oid.h | 39 +++++++ src/signed_data.c | 275 +++++++++++++++++++++++++++++++++++++++++++++ src/signed_data.h | 11 ++ src/tal.h | 2 + 15 files changed, 669 insertions(+), 12 deletions(-) create mode 100644 src/content_info.c create mode 100644 src/content_info.h create mode 100644 src/file.c create mode 100644 src/file.h create mode 100644 src/oid.c create mode 100644 src/oid.h create mode 100644 src/signed_data.c create mode 100644 src/signed_data.h diff --git a/autogen.sh b/autogen.sh index 042d19df..c06a9249 100755 --- a/autogen.sh +++ b/autogen.sh @@ -4,4 +4,4 @@ # Run this file to generate the configure script. # You'll need Autoconf and Automake installed! -aclocal && automake --add-missing --copy && autoconf +autoreconf --install diff --git a/configure.ac b/configure.ac index d65fdfb2..f01b3f71 100644 --- a/configure.ac +++ b/configure.ac @@ -23,11 +23,16 @@ AC_CHECK_HEADER_STDBOOL # Checks for library functions. AC_FUNC_MALLOC AC_CHECK_FUNCS([memset socket]) -AC_SEARCH_LIBS([pthread_create], [pthread]) +AC_SEARCH_LIBS([pthread_create], [pthread], [], + [AC_MSG_ERROR([unable to find the pthread() function])] +) +AC_SEARCH_LIBS([ber_decode], [cmscodec], [], + [AC_MSG_ERROR([unable to find the ber_decode() function])] +) # Check dependencies. +#PKG_CHECK_MODULES([OPENSSL], [openssl]) PKG_CHECK_MODULES([CHECK], [check]) -PKG_CHECK_MODULES([GLIB], [glib-2.0]) # Spit out the makefiles. AC_OUTPUT(Makefile src/Makefile test/Makefile) diff --git a/src/Makefile.am b/src/Makefile.am index 36722ef6..8e0f5e77 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -2,10 +2,10 @@ bin_PROGRAMS = rpki_validator rpki_validator_SOURCES = main.c rpki_validator_SOURCES += common.h -rpki_validator_SOURCES += line_file.h -rpki_validator_SOURCES += line_file.c -rpki_validator_SOURCES += tal.h -rpki_validator_SOURCES += tal.c +rpki_validator_SOURCES += content_info.h content_info.c +rpki_validator_SOURCES += file.h file.c +rpki_validator_SOURCES += oid.h oid.c +rpki_validator_SOURCES += signed_data.h signed_data.c -rpki_validator_CFLAGS = -pedantic -Wall -std=gnu11 -O3 ${GLIB_CFLAGS} -rpki_validator_LDADD = ${GLIB_LIBS} +rpki_validator_CFLAGS = -pedantic -Wall -std=gnu11 -O0 -g ${OPENSSL_CFLAGS} +rpki_validator_LDADD = ${OPENSSL_LIBS} diff --git a/src/common.h b/src/common.h index 9c266206..c682919a 100644 --- a/src/common.h +++ b/src/common.h @@ -1,6 +1,19 @@ #ifndef SRC_RTR_COMMON_H_ #define SRC_RTR_COMMON_H_ +#include + #define ARRAY_SIZE(array) (sizeof(array) / sizeof(array[0])) +#define warnxerror0(error, msg) \ + warnx(msg ": %s", strerror(error)) +#define warnxerrno0(msg) \ + warnxerror0(errno, msg) +#define warnxerror(error, msg, ...) \ + warnx(msg ": %s", ##__VA_ARGS__, strerror(error)) +#define warnxerrno(msg, ...) \ + warnxerror(errno, msg, ##__VA_ARGS__) + +#define pr_debug(msg, ...) printf("Debug: " msg "\n", ##__VA_ARGS__); + #endif /* SRC_RTR_COMMON_H_ */ diff --git a/src/content_info.c b/src/content_info.c new file mode 100644 index 00000000..6bb3295e --- /dev/null +++ b/src/content_info.c @@ -0,0 +1,115 @@ +#include "content_info.h" + +#include +#include +#include +#include "common.h" +#include "file.h" + +static void +content_type_print(FILE *stream, asn_oid_arc_t *arcs, unsigned int arc_count) +{ + unsigned int i; + + for (i = 0; i < arc_count; i++) { + fprintf(stream, "%u", arcs[i]); + if (i != arc_count - 1) + fprintf(stream, "."); + } +} + +int +content_type_validate(ContentType_t *ctype) +{ + asn_oid_arc_t expected[] = { 1, 2, 840, 113549, 1, 7, 2 }; + asn_oid_arc_t actual[ARRAY_SIZE(expected)]; + const unsigned int SLOTS = ARRAY_SIZE(expected); + ssize_t result; + unsigned int i; + + result = OBJECT_IDENTIFIER_get_arcs(ctype, actual, SLOTS); + if (result != SLOTS) + goto failure; + + for (i = 0; i < SLOTS; i++) { + if (expected[i] != actual[i]) + goto failure; + } + + return 0; + +failure: + fprintf(stderr, "Incorrect content-type; expected "); + content_type_print(stderr, expected, SLOTS); + fprintf(stderr, ", got "); + content_type_print(stderr, actual, (result < SLOTS) ? result : SLOTS); + fprintf(stderr, ".\n"); + return -EINVAL; +} + +static int +validate(struct ContentInfo *info) +{ + char error_msg[256]; + size_t error_msg_size; + int error; + + error_msg_size = sizeof(error_msg); + error = asn_check_constraints(&asn_DEF_ContentInfo, info, error_msg, + &error_msg_size); + if (error == -1) { + warnx("Error validating content info object: %s", error_msg); + return -EINVAL; + } + + return content_type_validate(&info->contentType); +} + +static int +decode(struct file_contents *fc, struct ContentInfo **result) +{ + struct ContentInfo *info = NULL; + asn_dec_rval_t rval; + int error; + + rval = ber_decode(0, &asn_DEF_ContentInfo, (void **) &info, fc->buffer, + fc->buffer_size); + if (rval.code != RC_OK) { + warnx("Error decoding content info object: %d", rval.code); + /* Must free partial content info according to API contracts. */ + content_info_free(info); + return -EINVAL; + } + + error = validate(info); + if (error) { + content_info_free(info); + return error; + } + + *result = info; + return 0; +} + +int +content_info_load(const char *file_name, struct ContentInfo **result) +{ + struct file_contents fc; + int error; + + error = file_load(file_name, &fc); + if (error) + return error; + + error = decode(&fc, result); + + file_free(&fc); + return error; +} + +void +content_info_free(struct ContentInfo *info) +{ + asn_DEF_ContentInfo.op->free_struct(&asn_DEF_ContentInfo, info, + ASFM_FREE_EVERYTHING); +} diff --git a/src/content_info.h b/src/content_info.h new file mode 100644 index 00000000..87c7e619 --- /dev/null +++ b/src/content_info.h @@ -0,0 +1,11 @@ +#ifndef SRC_CONTENT_INFO_H_ +#define SRC_CONTENT_INFO_H_ + +/* Some wrappers for libcmscodec's ContentInfo. */ + +#include + +int content_info_load(const char *file_name, struct ContentInfo **result); +void content_info_free(struct ContentInfo *info); + +#endif /* SRC_CONTENT_INFO_H_ */ diff --git a/src/file.c b/src/file.c new file mode 100644 index 00000000..ea4d9004 --- /dev/null +++ b/src/file.c @@ -0,0 +1,88 @@ +#include "file.h" + +#include +#include +#include +#include +#include "common.h" + +/* + * Will also rewind the file as a side effect. + * This is currently perfect for calling users. + */ +static int +get_file_size(FILE *file, long int *size) +{ + if (fseek(file, 0L, SEEK_END) == -1) + return errno ? errno : -EINVAL; + *size = ftell(file); + rewind(file); + return 0; +} + +int +file_load(const char *file_name, struct file_contents *fc) +{ + FILE *file; + long int file_size; + size_t fread_result; + int error; + + file = fopen(file_name, "rb"); + if (file == NULL) { + warnxerrno("Could not open file '%s'", file_name); + return errno; + } + + error = get_file_size(file, &file_size); + if (error) { + warnxerror0(error, "Could not compute file size"); + fclose(file); + return error; + } + + fc->buffer_size = file_size; + fc->buffer = malloc(fc->buffer_size); + if (fc->buffer == NULL) { + warnx("Out of memory."); + fclose(file); + return -ENOMEM; + } + + fread_result = fread(fc->buffer, 1, fc->buffer_size, file); + if (fread_result < fc->buffer_size) { + error = ferror(file); + if (error) { + /* + * The manpage doesn't say that the result is an error + * code. It literally doesn't say how to obtain the + * error code. + */ + warnx("File read error. The errcode is presumably %d. (%s)", + error, strerror(error)); + free(fc->buffer); + fclose(file); + return error; + } + + /* + * As far as I can tell from the man page, feof() cannot return + * less bytes that requested like read() does. + */ + warnx("Likely programming error: fread() < file size"); + warnx("fr:%zu bs:%zu EOF:%d", fread_result, fc->buffer_size, + feof(file)); + free(fc->buffer); + fclose(file); + return -EINVAL; + } + + fclose(file); + return 0; +} + +void +file_free(struct file_contents *fc) +{ + free(fc->buffer); +} diff --git a/src/file.h b/src/file.h new file mode 100644 index 00000000..3ee0aa8c --- /dev/null +++ b/src/file.h @@ -0,0 +1,19 @@ +#ifndef SRC_FILE_H_ +#define SRC_FILE_H_ + +#include + +/* + * The entire contents of the file, loaded into a buffer. + * + * Instances of this struct are expected to live on the stack. + */ +struct file_contents { + unsigned char *buffer; + size_t buffer_size; +}; + +int file_load(const char *file_name, struct file_contents *fc); +void file_free(struct file_contents *fc); + +#endif /* SRC_FILE_H_ */ diff --git a/src/line_file.c b/src/line_file.c index 59cb6660..e723c1e9 100644 --- a/src/line_file.c +++ b/src/line_file.c @@ -119,7 +119,7 @@ lfile_read(struct line_file *lfile, char **result) */ for (i = 0; i < len; i++) { if (string[i] == '\0') { - warnx("File %s has an illegal null character in its body. Please remove it.", + warnx("File '%s' has an illegal null character in its body. Please remove it.", lfile_name(lfile)); free(string); return -EINVAL; diff --git a/src/main.c b/src/main.c index 94f713be..19c52108 100644 --- a/src/main.c +++ b/src/main.c @@ -1,7 +1,31 @@ -#include +#include "content_info.h" +#include "signed_data.h" + +const char *FILE_NAME = "/home/ydahhrk/rpki-cache/repository/" + "ca.rg.net/rpki/RGnet/IZt-j9P0XqJjzM2Xi4RZKS60gOc.roa"; int main(void) { - return EXIT_SUCCESS; + struct ContentInfo *cinfo; + struct SignedData *sdata; + int error; + + error = content_info_load(FILE_NAME, &cinfo); + if (error) + return error; + + error = signed_data_decode(&cinfo->content, &sdata); + if (error) { + content_info_free(cinfo); + return error; + } + +// asn_fprint(stdout, &asn_DEF_ContentInfo, cinfo); +// printf("---------------------------------------------\n"); +// asn_fprint(stdout, &asn_DEF_SignedData, sdata); + + signed_data_free(sdata); + content_info_free(cinfo); + return 0; } diff --git a/src/oid.c b/src/oid.c new file mode 100644 index 00000000..b0867bf4 --- /dev/null +++ b/src/oid.c @@ -0,0 +1,55 @@ +#include "oid.h" + +#include +#include "common.h" + +#define MAX_ARCS 9 + +/* Please update MAX_ARCS if you add an OID that has more arcs. */ +static asn_oid_arc_t OID_SHA224[] = { 2, 16, 840, 1, 101, 3, 4, 2, 4 }; +static asn_oid_arc_t OID_SHA256[] = { 2, 16, 840, 1, 101, 3, 4, 2, 1 }; +static asn_oid_arc_t OID_SHA384[] = { 2, 16, 840, 1, 101, 3, 4, 2, 2 }; +static asn_oid_arc_t OID_SHA512[] = { 2, 16, 840, 1, 101, 3, 4, 2, 3 }; + +/* + * @a_oid is the original OID that's being tested. + * @a_arcs must be a stack-allocated array of size @len. + * @b_arcs is the expected array of arcs that needs to be compared to @a_oid. + * Its length must be @len. + */ +bool +oid_equals(OBJECT_IDENTIFIER_t *const actual_oid, + asn_oid_arc_t const *expected_arcs, + size_t len) +{ + asn_oid_arc_t actual_arcs[MAX_ARCS]; + ssize_t count; + long int i; + + count = OBJECT_IDENTIFIER_get_arcs(actual_oid, actual_arcs, len); + if (count != len) + return false; + + /* Most OIDs start with the same numbers, so iterate backwards. */ + for (i = len - 1; i >= 0; i--) { + if (actual_arcs[i] != expected_arcs[i]) + return false; + } + + return true; +} + +void +oid_print(OBJECT_IDENTIFIER_t *oid) +{ + asn_fprint(stdout, &asn_DEF_OBJECT_IDENTIFIER, oid); +} + +bool +is_digest_algorithm(AlgorithmIdentifier_t *algorithm) +{ + return OID_EQUALS(&algorithm->algorithm, OID_SHA224) + || OID_EQUALS(&algorithm->algorithm, OID_SHA256) + || OID_EQUALS(&algorithm->algorithm, OID_SHA384) + || OID_EQUALS(&algorithm->algorithm, OID_SHA512); +} diff --git a/src/oid.h b/src/oid.h new file mode 100644 index 00000000..f8985042 --- /dev/null +++ b/src/oid.h @@ -0,0 +1,39 @@ +#ifndef SRC_OID_H_ +#define SRC_OID_H_ + +#include +#include "libcmscodec/AlgorithmIdentifier.h" + +#include "common.h" + +typedef asn_oid_arc_t OID[]; + +/* Please update MAX_ARCS if you add an OID that has more arcs. */ +static const OID CONTENT_TYPE_ATTR_OID = { + 1, 2, 840, 113549, 1, 9, 3 +}; +static const OID MESSAGE_DIGEST_ATTR_OID = { + 1, 2, 840, 113549, 1, 9, 4 +}; +static const OID SIGNING_TIME_ATTR_OID = { + 1, 2, 840, 113549, 1, 9, 5 +}; +static const OID BINARY_SIGNING_TIME_ATTR_OID = { + 1, 2, 840, 113549, 1, 9, 16, 2, 46 +}; + +/* Use OID_EQUALS() instead. */ +bool oid_equals(OBJECT_IDENTIFIER_t *const actual_oid, + asn_oid_arc_t const *expected_arcs, size_t len); + +/* + * a is supposed to be a OBJECT_IDENTIFIER_t (from libcmscodec.) + * b is supposed to be an OID (from the typedef above.) + */ +#define OID_EQUALS(a, b) oid_equals(a, b, ARRAY_SIZE(b)) + +void oid_print(OBJECT_IDENTIFIER_t *oid); + +bool is_digest_algorithm(AlgorithmIdentifier_t *algorithm); + +#endif /* SRC_OID_H_ */ diff --git a/src/signed_data.c b/src/signed_data.c new file mode 100644 index 00000000..187047f2 --- /dev/null +++ b/src/signed_data.c @@ -0,0 +1,275 @@ +#include "signed_data.h" + +#include +#include +#include +#include "oid.h" + +/* TODO more consistent and informative error/warning messages.*/ + +static int +validate_content_type_attribute(CMSAttributeValue_t *value, + EncapsulatedContentInfo_t *eci) +{ + /* TODO need to decode value. */ + + /* eci->eContentType*/ + + return 0; +} + +static int +validate_message_digest_attribute(CMSAttributeValue_t *value) +{ + return 0; /* TODO need the content being signed */ +} + +static int +validate_signed_attrs(struct SignerInfo *sinfo, EncapsulatedContentInfo_t *eci) +{ + struct CMSAttribute *attr; + struct CMSAttribute__attrValues *attrs; + unsigned int i; + bool content_type_found = false; + bool message_digest_found = false; + bool signing_time_found = false; + bool binary_signing_time_found = false; + int error; + + if (sinfo->signedAttrs == NULL) { + warnx("The SignerInfo's signedAttrs field is NULL."); + return -EINVAL; + } + + for (i = 0; i < sinfo->signedAttrs->list.count; i++) { + attr = sinfo->signedAttrs->list.array[i]; + if (attr == NULL) { + warnx("SignedAttrs array element %u is NULL.", i); + continue; + } + attrs = &attr->attrValues; + + if (attrs->list.count != 1) { + warnx("signedAttrs's attribute set size (%d) is different than 1.", + attr->attrValues.list.count); + return -EINVAL; + } + if (attrs->list.array == NULL || attrs->list.array[0] == NULL) { + warnx("Programming error: Array size is 1 but array itself is NULL."); + return -EINVAL; + } + + if (OID_EQUALS(&attr->attrType, CONTENT_TYPE_ATTR_OID)) { + if (content_type_found) { + warnx("Multiple ContentTypes found."); + return -EINVAL; + } + error = validate_content_type_attribute(attr->attrValues.list.array[0], eci); + content_type_found = true; + + } else if (OID_EQUALS(&attr->attrType, MESSAGE_DIGEST_ATTR_OID)) { + if (message_digest_found) { + warnx("Multiple MessageDigests found."); + return -EINVAL; + } + error = validate_message_digest_attribute(attr->attrValues.list.array[0]); + message_digest_found = true; + + } else if (OID_EQUALS(&attr->attrType, SIGNING_TIME_ATTR_OID)) { + if (signing_time_found) { + warnx("Multiple SigningTimes found."); + return -EINVAL; + } + error = 0; /* No validations needed for now. */ + signing_time_found = true; + + } else if (OID_EQUALS(&attr->attrType, BINARY_SIGNING_TIME_ATTR_OID)) { + if (binary_signing_time_found) { + warnx("Multiple BinarySigningTimes found."); + return -EINVAL; + } + error = 0; /* No validations needed for now. */ + binary_signing_time_found = true; + + } else { + warnx("Illegal attrType OID in SignerInfo."); + return -EINVAL; + } + + if (error) + return error; + } + + if (!content_type_found) { + warnx("SignerInfo lacks a ContentType attribute."); + return -EINVAL; + } + if (!message_digest_found) { + warnx("SignerInfo lacks a MessageDigest attribute."); + return -EINVAL; + } + + return 0; +} + +static int +validate(struct SignedData *sdata) +{ + char error_msg[256]; + size_t error_msg_size; + int error; + struct SignerInfo *sinfo; + + /* The lib's inbuilt validations. (Probably not much.) */ + error_msg_size = sizeof(error_msg); + error = asn_check_constraints(&asn_DEF_SignedData, sdata, error_msg, + &error_msg_size); + if (error == -1) { + warnx("Error validating SignedData object: %s", error_msg); + return -EINVAL; + } + + /* rfc6488#section-2.1 */ + if (sdata->signerInfos.list.count != 1) { + warnx("The SignedData's SignerInfo set is supposed to have only one element. (%d given.)", + sdata->signerInfos.list.count); + return -EINVAL; + } + + /* rfc6488#section-2.1.1 */ + if (sdata->version != 3) { + warnx("The SignedData version is only allowed to be 3. (Was %ld.)", + sdata->version); + return -EINVAL; + } + + /* rfc6488#section-2.1.2 */ + if (sdata->digestAlgorithms.list.count != 1) { + warnx("The SignedData's digestAlgorithms set is supposed to have only one element. (%d given.)", + sdata->digestAlgorithms.list.count); + return -EINVAL; + } + + /* + * No idea what to do with struct DigestAlgorithmIdentifier; it's not + * defined anywhere and the code always seems to fall back to + * AlgorithmIdentifier instead. There's no API. + * This seems to work fine. + */ + if (!is_digest_algorithm((DigestAlgorithmIdentifier_t *) sdata->digestAlgorithms.list.array[0])) { + warnx("The SignedData's digestAlgorithm OID is not listed in RFC 5754."); + return -EINVAL; + } + + /* section-2.1.3 */ + /* TODO need a callback for specific signed object types */ + + /* rfc6488#section-2.1.4 */ + if (sdata->certificates == NULL) { + warnx("The SignedData does not contain certificates."); + return -EINVAL; + } + + if (sdata->certificates->list.count != 1) { + warnx("The SignedData contains %d certificates, one expected.", + sdata->certificates->list.count); + return -EINVAL; + } + + /* rfc6488#section-2.1.5 */ + if (sdata->crls != NULL && sdata->crls->list.count > 0) { + warnx("The SignedData contains at least one crls."); + return -EINVAL; + } + + /* rfc6488#section-2.1.6.1 */ + sinfo = sdata->signerInfos.list.array[0]; + if (sinfo == NULL) { + warnx("The SignerInfo object is NULL."); + return -EINVAL; + } + if (sinfo->version != 3) { + warnx("The SignerInfo version is only allowed to be 3. (Was %ld.)", + sinfo->version); + return -EINVAL; + } + + /* rfc6488#section-2.1.6.2 */ + /* + * TODO need the "EE certificate carried in the CMS certificates field." + */ + + /* rfc6488#section-2.1.6.3 */ + if (!is_digest_algorithm((AlgorithmIdentifier_t *) &sinfo->digestAlgorithm)) { + warnx("The SignerInfo digestAlgorithm OID is not listed in RFC 5754."); + return -EINVAL; + } + + /* rfc6488#section-2.1.6.4 */ + error = validate_signed_attrs(sinfo, &sdata->encapContentInfo); + if (error) + return error; + + /* rfc6488#section-2.1.6.5 */ + /* + * RFC 6485 was obsoleted by 7935. 7935 simply refers to 5652. + * + * RFC 5652: + * + * > Since each signer can employ a different digital signature + * > technique, and future specifications could update the syntax, all + * > implementations MUST gracefully handle unimplemented versions of + * > SignerInfo. Further, since all implementations will not support + * > every possible signature algorithm, all implementations MUST + * > gracefully handle unimplemented signature algorithms when they are + * > encountered. + * + * So, nothing to do for now. + */ + + /* rfc6488#section-2.1.6.6 */ + /* Again, nothing to do for now. */ + + /* rfc6488#section-2.1.6.7 */ + if (sinfo->unsignedAttrs != NULL && sinfo->unsignedAttrs->list.count > 0) { + warnx("SignerInfo has at least one unsignedAttr."); + return -EINVAL; + } + + /* TODO section 3 */ + + return 0; +} + +int +signed_data_decode(ANY_t *coded, struct SignedData **result) +{ + struct SignedData *sdata = NULL; + asn_dec_rval_t rval; + int error; + + rval = ber_decode(0, &asn_DEF_SignedData, (void **) &sdata, coded->buf, + coded->size); + if (rval.code != RC_OK) { + warnx("Error decoding signed data object: %d", rval.code); + /* Must free partial signed data according to API contracts. */ + signed_data_free(sdata); + return -EINVAL; + } + + error = validate(sdata); + if (error) { + signed_data_free(sdata); + return error; + } + + *result = sdata; + return 0; +} + +void +signed_data_free(struct SignedData *sdata) +{ + asn_DEF_SignedData.op->free_struct(&asn_DEF_SignedData, sdata, + ASFM_FREE_EVERYTHING); +} diff --git a/src/signed_data.h b/src/signed_data.h new file mode 100644 index 00000000..22923d4d --- /dev/null +++ b/src/signed_data.h @@ -0,0 +1,11 @@ +#ifndef SRC_SIGNED_DATA_H_ +#define SRC_SIGNED_DATA_H_ + +/* Some wrappers for libcmscodec's SignedData. */ + +#include + +int signed_data_decode(ANY_t *coded, struct SignedData **result); +void signed_data_free(struct SignedData *sdata); + +#endif /* SRC_SIGNED_DATA_H_ */ diff --git a/src/tal.h b/src/tal.h index 9dfa869b..79205cb5 100644 --- a/src/tal.h +++ b/src/tal.h @@ -1,6 +1,8 @@ #ifndef TAL_H_ #define TAL_H_ +/* This is RFC 7730. */ + struct tal; int tal_load(const char *, struct tal **); -- 2.47.3