From b187ae70c2ea54784e041680210bf0265e9db2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Trojnara?= Date: Wed, 30 Sep 2026 13:09:21 +0200 Subject: [PATCH 1/2] Support multiple timestamps per signature Preserve existing timestamps when adding new ones and verify every value of both Authenticode and RFC 3161 timestamp attributes. Accept a signature when at least one trusted timestamp and the Authenticode signature both validate at that timestamp's time. Malformed or untrusted alternatives must not hide a valid pair or be treated as absent timestamps. Add -timestamp-all to require every requested TSA, allowing both remote protocols together without changing the existing URL fallback behavior. Reject -timestamp-all without any -t or -ts URL, so it never succeeds without adding a timestamp. Reject excess URLs before writing past the timestamp server arrays and use a while loop for variable-length argument consumption. Document the new verification policy and the change from implicit timestamp replacement to appending. --- NEWS.md | 4 + README.md | 14 +++ cmake/CMakeTest.cmake | 27 +++++ osslsigncode.c | 274 +++++++++++++++++++++--------------------- osslsigncode.h | 1 + osslsigncode.md | 43 +++++-- 6 files changed, 218 insertions(+), 145 deletions(-) diff --git a/NEWS.md b/NEWS.md index 454f017..0b3b049 100644 --- a/NEWS.md +++ b/NEWS.md @@ -14,6 +14,10 @@ - fixed heap out-of-bounds reads when parsing malformed APPX ZIP64 extra fields - fixed heap out-of-bounds reads when verifying malformed PKCS#7 signature attributes - fixed undefined behavior when copying an empty MSI signature blob +- added multiple timestamp support: `add` preserves existing timestamps and + `-timestamp-all` requires every specified Authenticode/RFC 3161 server +- verification now tries all timestamps and accepts any trusted timestamp for + which the Authenticode signature also validates at the timestamp's time ### 2.14 (2026.07.20) diff --git a/README.md b/README.md index 360cff9..1406920 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,20 @@ or if you want to add a timestamp as well: -t http://timestamp.digicert.com \ -in yourapp.exe -out yourapp-signed.exe ``` +To require multiple timestamps, use `-timestamp-all` with repeated `-ts` or +`-t` options (both protocols may be combined): +``` + osslsigncode add -timestamp-all \ + -ts https://tsa1.example.com/ -ts https://tsa2.example.com/ \ + -in yourapp-signed.exe -out yourapp-multistamped.exe +``` +Every requested server must succeed. Without `-timestamp-all`, repeated URLs +remain fallback servers, stopping after the first success. `add` preserves +existing timestamps. Verification succeeds if at least one trusted timestamp +and the signing certificate/signature validate at that timestamp's time; +invalid alternatives do not invalidate a valid pair. Use `-ignore-timestamp` +to verify at the requested/current time instead. + You can use a certificate and key stored in a PKCS#12 container: ``` osslsigncode sign -pkcs12 -pass \ diff --git a/cmake/CMakeTest.cmake b/cmake/CMakeTest.cmake index 6b7a6f9..4a8cd8b 100644 --- a/cmake/CMakeTest.cmake +++ b/cmake/CMakeTest.cmake @@ -544,6 +544,20 @@ if(Python3_FOUND AND NOT cryptography_error) endforeach(cert ${pem_certs}) endforeach(ext ${extensions_all}) + # Append two more RFC3161 timestamps to the existing one + add_test(NAME "add_ts_all_exe" + COMMAND ${Python3_EXECUTABLE} ${EXEC} ${OSSLSIGNCODE} "add" + "-timestamp-all" + "-ts" "FILE ${LOGS}/url.log" + "-ts" "FILE ${LOGS}/url.log" + "-in" "${FILES}/ts_cert.exe" + "-out" "${FILES}/ts_all.exe") + set_tests_properties("add_ts_all_exe" PROPERTIES + ENVIRONMENT "HTTP_PROXY=;http_proxy=" + DEPENDS "sign_ts_cert_exe" + RESOURCE_LOCK "tsa") + list(APPEND ALL_TESTS "add_ts_all_exe") + ### Verify Time-Stamp Authority ### @@ -561,6 +575,18 @@ if(Python3_FOUND AND NOT cryptography_error) list(APPEND ALL_TESTS "verify_ts_cert_${ext}") endforeach(ext ${extensions_all}) + # Verify a signature with multiple timestamps + add_test(NAME "verify_ts_all_exe" + COMMAND ${Python3_EXECUTABLE} ${EXEC} ${OSSLSIGNCODE} "verify" + "-time" "1567296000" # Signature verification time: Sep 1 00:00:00 2019 GMT + "-CAfile" "${CERTS}/CACert.pem" + "-TSA-CAfile" "${CERTS}/TSACA.pem" + "-in" "${FILES}/ts_all.exe") + set_tests_properties("verify_ts_all_exe" PROPERTIES + ENVIRONMENT "HTTP_PROXY=;http_proxy=;" + DEPENDS "add_ts_all_exe") + list(APPEND ALL_TESTS "verify_ts_all_exe") + # Verify with untrusted Time-Stamp Authority # This test is expected to fail foreach(ext ${extensions_all}) @@ -718,6 +744,7 @@ if(Python3_FOUND AND NOT cryptography_error) set(OUTPUT_FILES ${OUTPUT_FILES} "${FILES}/jreq.tsq") set(OUTPUT_FILES ${OUTPUT_FILES} "${FILES}/jresp.tsr") endforeach(ext ${extensions_all}) + set(OUTPUT_FILES ${OUTPUT_FILES} "${FILES}/ts_all.exe") add_test(NAME "remove_files" COMMAND ${CMAKE_COMMAND} -E rm -f ${OUTPUT_FILES}) diff --git a/osslsigncode.c b/osslsigncode.c index 320e930..7919488 100644 --- a/osslsigncode.c +++ b/osslsigncode.c @@ -1079,33 +1079,26 @@ static int add_timestamp(PKCS7 *p7, FILE_FORMAT_CTX *ctx, char *url, int rfc3161 } /* - * [in, out] p7: new PKCS#7 signature - * [in] ctx: structure holds input and output data + * Try fallback servers, or require every server with -timestamp-all. * [returns] 0 on error or 1 on success */ -static int add_timestamp_authenticode(PKCS7 *p7, FILE_FORMAT_CTX *ctx) +static int add_timestamps(PKCS7 *p7, FILE_FORMAT_CTX *ctx, int rfc3161) { + char **urls = rfc3161 ? ctx->options->tsurl : ctx->options->turl; + int count = rfc3161 ? ctx->options->ntsurl : ctx->options->nturl; int i; - for (i=0; ioptions->nturl; i++) { - if (!add_timestamp(p7, ctx, ctx->options->turl[i], 0)) - return 1; /* OK */ - } - return 0; /* FAILED */ -} -/* - * [in, out] p7: new PKCS#7 signature - * [in] ctx: structure holds input and output data - * [returns] 0 on error or 1 on success - */ -static int add_timestamp_rfc3161(PKCS7 *p7, FILE_FORMAT_CTX *ctx) -{ - int i; - for (i=0; ioptions->ntsurl; i++) { - if (!add_timestamp(p7, ctx, ctx->options->tsurl[i], 1)) + for (i = 0; i < count; i++) { + int failed = add_timestamp(p7, ctx, urls[i], rfc3161); + + if (ctx->options->timestamp_all) { + if (failed) + return 0; /* FAILED */ + } else if (!failed) { return 1; /* OK */ + } } - return 0; /* FAILED */ + return ctx->options->timestamp_all; } /* @@ -1466,12 +1459,12 @@ static int add_unauthenticated_blob(PKCS7 *p7, const char *blob_file) static int add_timestamp_and_blob(PKCS7 *p7, FILE_FORMAT_CTX *ctx) { /* add counter-signature/timestamp */ - if (ctx->options->nturl && !add_timestamp_authenticode(p7, ctx)) { + if (ctx->options->nturl && !add_timestamps(p7, ctx, 0)) { fprintf(stderr, "%s\n%s\n", "Authenticode timestamping failed", "Use the \"-ts\" option to add the RFC3161 Time-Stamp Authority or choose another one Authenticode Time-Stamp Authority"); return 1; /* FAILED */ } - if (ctx->options->ntsurl && !add_timestamp_rfc3161(p7, ctx)) { + if (ctx->options->ntsurl && !add_timestamps(p7, ctx, 1)) { fprintf(stderr, "%s\n%s\n", "RFC 3161 timestamping failed", "Use the \"-t\" option to add the Authenticode Time-Stamp Authority or choose another one RFC3161 Time-Stamp Authority"); return 1; /* FAILED */ @@ -1651,14 +1644,6 @@ static int X509_attribute_chain_append_object(STACK_OF(X509_ATTRIBUTE) **unauth_ continue; object_txt[0] = 0x00; OBJ_obj2txt(object_txt, sizeof object_txt, object, 1); - if ((!strcmp(oid, PKCS9_COUNTER_SIGNATURE) || !strcmp(oid, SPC_RFC3161_OBJID)) - && (!strcmp(object_txt, PKCS9_COUNTER_SIGNATURE) || !strcmp(object_txt, SPC_RFC3161_OBJID))) { - /* free up countersignature/timestamp in unauthenticated attributes - * to override the previous timestamp */ - X509at_delete_attr(*unauth_attr, i); - X509_ATTRIBUTE_free(attr); - continue; - } if (!strcmp(oid, object_txt)) { /* append p to the V_ASN1_SEQUENCE */ if (!X509_ATTRIBUTE_set1_data(attr, V_ASN1_SEQUENCE, p, len)) @@ -2694,14 +2679,11 @@ static int print_cms_timestamp(CMS_ContentInfo *timestamp, time_t time) } /* - * RFC3852: the message-digest authenticated attribute type MUST be - * present when there are any authenticated attributes present - * [in] timestamp: CMS_ContentInfo struct for Authenticode Timestamp or RFC 3161 Timestamp + * Print signature attributes; timestamps are handled separately during verification. * [in] p7: PKCS#7 signature * [in] verbose: additional output mode - * [returns] 0 on error or 1 on success */ -static time_t time_t_timestamp_get_attributes(CMS_ContentInfo **timestamp, PKCS7 *p7, int verbose) +static void print_signature_attributes(PKCS7 *p7, int verbose) { STACK_OF(PKCS7_SIGNER_INFO) *signer_info; PKCS7_SIGNER_INFO *si; @@ -2712,14 +2694,13 @@ static time_t time_t_timestamp_get_attributes(CMS_ContentInfo **timestamp, PKCS7 const ASN1_STRING *value; const u_char *data; char object_txt[128]; - time_t time = INVALID_TIME; signer_info = PKCS7_get_signer_info(p7); if (!signer_info) - return INVALID_TIME; /* FAILED */ + return; si = sk_PKCS7_SIGNER_INFO_value(signer_info, 0); if (!si) - return INVALID_TIME; /* FAILED */ + return; md_nid = OBJ_obj2nid(si->digest_alg->algorithm); printf("Message digest algorithm: %s\n", (md_nid == NID_undef) ? "UNKNOWN" : OBJ_nid2sn(md_nid)); @@ -2841,68 +2822,7 @@ static time_t time_t_timestamp_get_attributes(CMS_ContentInfo **timestamp, PKCS7 continue; object_txt[0] = 0x00; OBJ_obj2txt(object_txt, sizeof object_txt, object, 1); - if (!strcmp(object_txt, PKCS9_COUNTER_SIGNATURE)) { - /* Authenticode Timestamp - Policy OID: 1.2.840.113549.1.9.6 */ - CMS_ContentInfo *cms; - PKCS7_SIGNER_INFO *countersi; - - value = (const ASN1_STRING *)X509_ATTRIBUTE_get0_data(attr, 0, V_ASN1_SEQUENCE, NULL); - if (value == NULL) - continue; - data = ASN1_STRING_get0_data(value); - countersi = d2i_PKCS7_SIGNER_INFO(NULL, &data, ASN1_STRING_length(value)); - if (countersi == NULL) { - printf("Warning: Authenticode Timestamp could not be decoded correctly\n"); - ERR_print_errors_fp(stderr); - continue; - } - time = time_t_get_si_time(countersi); - if (time != INVALID_TIME) { - cms = cms_get_timestamp(p7->d.sign, countersi); - if (cms) { - if (!print_cms_timestamp(cms, time)) { - CMS_ContentInfo_free(cms); - printf("Warning: Authenticode Timestamp could not be decoded correctly\n"); - ERR_print_errors_fp(stderr); - continue; - } - *timestamp = cms; - } else { - printf("Warning: Corrupt Authenticode Timestamp embedded content\n"); - } - } else { - printf("Warning: PKCS9_TIMESTAMP_SIGNING_TIME attribute not found\n"); - PKCS7_SIGNER_INFO_free(countersi); - } - } else if (!strcmp(object_txt, SPC_RFC3161_OBJID)) { - /* RFC3161 Timestamp - Policy OID: 1.3.6.1.4.1.311.3.3.1 */ - CMS_ContentInfo *cms; - - value = (const ASN1_STRING *)X509_ATTRIBUTE_get0_data(attr, 0, V_ASN1_SEQUENCE, NULL); - if (value == NULL) - continue; - data = ASN1_STRING_get0_data(value); - cms = d2i_CMS_ContentInfo(NULL, &data, ASN1_STRING_length(value)); - if (cms == NULL) { - printf("Warning: RFC3161 Timestamp could not be decoded correctly\n"); - ERR_print_errors_fp(stderr); - continue; - } - time = time_t_get_cms_time(cms); - if (time != INVALID_TIME) { - if (!print_cms_timestamp(cms, time)) { - CMS_ContentInfo_free(cms); - printf("Warning: RFC3161 Timestamp could not be decoded correctly\n"); - ERR_print_errors_fp(stderr); - continue; - } - *timestamp = cms; - } else { - printf("Warning: Corrupt RFC3161 Timestamp embedded content\n"); - CMS_ContentInfo_free(cms); - ERR_print_errors_fp(stderr); - } - } else if (!strcmp(object_txt, SPC_UNAUTHENTICATED_DATA_BLOB_OBJID)) { + if (!strcmp(object_txt, SPC_UNAUTHENTICATED_DATA_BLOB_OBJID)) { /* Unauthenticated Data Blob - Policy OID: 1.3.6.1.4.1.42921.1.2.1 */ value = (const ASN1_STRING *)X509_ATTRIBUTE_get0_data(attr, 0, V_ASN1_UTF8STRING, NULL); if (value == NULL) { @@ -2934,8 +2854,6 @@ static time_t time_t_timestamp_get_attributes(CMS_ContentInfo **timestamp, PKCS7 (md_nid == NID_undef) ? "UNKNOWN" : OBJ_nid2sn(md_nid)); print_hash("Signature", "", data, len); } - - return time; } /* @@ -3048,7 +2966,7 @@ static time_t time_t_get_cms_time(CMS_ContentInfo *cms) * Create new CMS_ContentInfo struct for Authenticode Timestamp. * This struct does not contain any TS_TST_INFO as specified in RFC 3161. * [in] p7_signed: PKCS#7 signedData structure - * [in] countersignature: Authenticode Timestamp decoded to PKCS7_SIGNER_INFO + * [in] countersignature: Authenticode Timestamp decoded to PKCS7_SIGNER_INFO (consumed) * [returns] pointer to CMS_ContentInfo structure */ static CMS_ContentInfo *cms_get_timestamp(PKCS7_SIGNED *p7_signed, @@ -3063,7 +2981,7 @@ static CMS_ContentInfo *cms_get_timestamp(PKCS7_SIGNED *p7_signed, p7 = PKCS7_new(); si = sk_PKCS7_SIGNER_INFO_value(p7_signed->signer_info, 0); - if (si == NULL) + if (!p7 || !si) goto out; /* Create new signed PKCS7 timestamp structure. */ @@ -3071,6 +2989,7 @@ static CMS_ContentInfo *cms_get_timestamp(PKCS7_SIGNED *p7_signed, goto out; if (!PKCS7_add_signer(p7, countersignature)) goto out; + countersignature = NULL; /* owned by p7 */ for (i = 0; i < sk_X509_num(p7_signed->cert); i++) { if (!PKCS7_add_certificate(p7, sk_X509_value(p7_signed->cert, i))) goto out; @@ -3100,6 +3019,7 @@ static CMS_ContentInfo *cms_get_timestamp(PKCS7_SIGNED *p7_signed, out: if (!cms) ERR_print_errors_fp(stderr); + PKCS7_SIGNER_INFO_free(countersignature); PKCS7_free(p7); return cms; } @@ -3218,6 +3138,97 @@ static int verify_content(FILE_FORMAT_CTX *ctx, PKCS7 *p7) return 1; /* FAILED */ } +/* Decode one timestamp value. The caller owns the returned CMS object. */ +static CMS_ContentInfo *timestamp_decode(PKCS7 *p7, X509_ATTRIBUTE *attr, + int index, int rfc3161, time_t *time) +{ + const ASN1_STRING *value; + const u_char *data, *end; + CMS_ContentInfo *cms = NULL; + int len; + + *time = INVALID_TIME; + value = X509_ATTRIBUTE_get0_data(attr, index, V_ASN1_SEQUENCE, NULL); + if (!value) + return NULL; + data = ASN1_STRING_get0_data(value); + len = ASN1_STRING_length(value); + if (!data || len <= 0) + return NULL; + end = data + len; + if (rfc3161) { + cms = d2i_CMS_ContentInfo(NULL, &data, len); + if (cms) + *time = time_t_get_cms_time(cms); + } else { + PKCS7_SIGNER_INFO *si = d2i_PKCS7_SIGNER_INFO(NULL, &data, len); + + if (si) { + *time = time_t_get_si_time(si); + /* cms_get_timestamp consumes si, including on failure. */ + cms = cms_get_timestamp(p7->d.sign, si); + } + } + if (data != end || *time == INVALID_TIME) { + CMS_ContentInfo_free(cms); + return NULL; + } + return cms; +} + +/* + * Try every value of every timestamp attribute. A timestamp is usable only if + * both it and the Authenticode signature validate at that timestamp's time. + * Missing timestamps (or -ignore-timestamp) use the requested/current time. + * [returns] 0 on error or 1 on success + */ +static int verify_signature_timestamps(FILE_FORMAT_CTX *ctx, PKCS7 *p7, X509 *signer) +{ + PKCS7_SIGNER_INFO *si = sk_PKCS7_SIGNER_INFO_value(PKCS7_get_signer_info(p7), 0); + STACK_OF(X509_ATTRIBUTE) *attrs = PKCS7_get_attributes(si); + int i, j, found = 0, verified = 0, count = 0; + + if (ctx->options->ignore_timestamp) { + printf("\nTimestamp Server Signature verification is disabled\n"); + return verify_authenticode(ctx, p7, INVALID_TIME, signer); + } + for (i = 0; i < X509at_get_attr_count(attrs); i++) { + X509_ATTRIBUTE *attr = X509at_get_attr(attrs, i); + int nid = OBJ_obj2nid(X509_ATTRIBUTE_get0_object(attr)); + int rfc3161 = nid == OBJ_txt2nid(SPC_RFC3161_OBJID); + + if (!rfc3161 && nid != OBJ_txt2nid(PKCS9_COUNTER_SIGNATURE)) + continue; + found = 1; /* Even an empty/malformed timestamp must not mean 'absent'. */ + if (X509_ATTRIBUTE_count(attr) == 0) + printf("Warning: Timestamp attribute contains no values\n"); + for (j = 0; j < X509_ATTRIBUTE_count(attr); j++) { + time_t time; + CMS_ContentInfo *timestamp = timestamp_decode(p7, attr, j, rfc3161, &time); + int timeok = 0; + + printf("\nTimestamp Index: %d\n", count++); + if (timestamp && print_cms_timestamp(timestamp, time)) + timeok = verify_timestamp(ctx, p7, timestamp, time); + else + printf("Warning: Timestamp could not be decoded correctly\n"); + printf("\nTimestamp Server Signature verification: %s\n", timeok ? "ok" : "failed"); + if (timeok) { + int verok = verify_authenticode(ctx, p7, time, signer); + + printf("Signature verification at timestamp time: %s\n", verok ? "ok" : "failed"); + verified |= verok; + } + CMS_ContentInfo_free(timestamp); + ERR_clear_error(); + } + } + if (found) + return verified; + printf("\nTimestamp is not available\n\n"); + return verify_authenticode(ctx, p7, INVALID_TIME, signer); +} + /* * [in] ctx: structure holds input and output data * [in] p7: PKCS#7 signature @@ -3225,11 +3236,9 @@ static int verify_content(FILE_FORMAT_CTX *ctx, PKCS7 *p7) */ static int verify_signature(FILE_FORMAT_CTX *ctx, PKCS7 *p7) { - int leafok, verok, timeok = 1; + int leafok, verok; STACK_OF(X509) *signers; X509 *signer; - CMS_ContentInfo *timestamp = NULL; - time_t time; signers = PKCS7_get0_signers(p7, NULL, 0); if (!signers || sk_X509_num(signers) != 1) { @@ -3241,7 +3250,7 @@ static int verify_signature(FILE_FORMAT_CTX *ctx, PKCS7 *p7) printf("Signer's certificate:\n"); print_cert(signer, 0); - time = time_t_timestamp_get_attributes(×tamp, p7, ctx->options->verbose); + print_signature_attributes(p7, ctx->options->verbose); if (ctx->options->leafhash != NULL) { leafok = verify_leaf_hash(signer, ctx->options->leafhash); printf("\nLeaf hash match: %s\n", leafok ? "ok" : "failed"); @@ -3259,24 +3268,9 @@ static int verify_signature(FILE_FORMAT_CTX *ctx, PKCS7 *p7) printf("TSA's certificates file: %s\n", ctx->options->tsa_cafile); if (ctx->options->tsa_crlfile) printf("TSA's CRL file: %s\n", ctx->options->tsa_crlfile); - if (timestamp) { - if (ctx->options->ignore_timestamp) { - printf("\nTimestamp Server Signature verification is disabled\n"); - time = INVALID_TIME; - } else { - timeok = verify_timestamp(ctx, p7, timestamp, time); - printf("\nTimestamp Server Signature verification: %s\n", timeok ? "ok" : "failed"); - if (!timeok) { - time = INVALID_TIME; - } - } - CMS_ContentInfo_free(timestamp); - ERR_clear_error(); - } else - printf("\nTimestamp is not available\n\n"); - verok = verify_authenticode(ctx, p7, time, signer); + verok = verify_signature_timestamps(ctx, p7, signer); printf("Signature verification: %s\n\n", verok ? "ok" : "failed"); - if (!timeok || !verok) + if (!verok) return 1; /* FAILED */ return 0; /* OK */ @@ -3711,8 +3705,8 @@ static void usage(const char *argv0, const char *cmd) printf("%12s[ -h {md5,sha1,sha2(56),sha384,sha512} ]\n", ""); printf("%12s[ -n ] [ -i ] [ -jp ] [ -comm ]\n", ""); printf("%12s[ -ph ]\n", ""); - printf("%12s[ -t [ -t ... ] [ -p ] [ -noverifypeer ]\n", ""); - printf("%12s[ -ts [ -ts ... ] [ -p ] [ -noverifypeer ] ]\n", ""); + printf("%12s[ -t [ -t ... ] ] [ -ts [ -ts ... ] ]\n", ""); + printf("%12s[ -timestamp-all ] [ -p ] [ -noverifypeer ]\n", ""); printf("%12s[ -TSA-certs ] [ -TSA-key ]\n", ""); printf("%12s[ -TSA-time ]\n", ""); printf("%12s[ -HTTPS-CAfile ]\n", ""); @@ -3734,8 +3728,8 @@ static void usage(const char *argv0, const char *cmd) } if (on_list(cmd, cmds_add)) { printf("%1sadd [ -addUnauthenticatedBlob [ -blobFile ] ]\n", ""); - printf("%12s[ -t [ -t ... ] [ -p ] [ -noverifypeer ]\n", ""); - printf("%12s[ -ts [ -ts ... ] [ -p ] [ -noverifypeer ] ]\n", ""); + printf("%12s[ -t [ -t ... ] ] [ -ts [ -ts ... ] ]\n", ""); + printf("%12s[ -timestamp-all ] [ -p ] [ -noverifypeer ]\n", ""); printf("%12s[ -TSA-certs ] [ -TSA-key ]\n", ""); printf("%12s[ -TSA-time ]\n", ""); printf("%12s[ -HTTPS-CAfile ]\n", ""); @@ -4008,11 +4002,13 @@ static void help_for(const char *argv0, const char *cmd) if (on_list(cmd, cmds_t)) { printf("%-24s= specifies that the digital signature will be timestamped\n", "-t"); printf("%26sby the Time-Stamp Authority (TSA) indicated by the URL\n", ""); - printf("%26sthis option cannot be used with the -ts option\n", ""); + printf("%26sURLs are tried in order until one succeeds (unless -timestamp-all is used)\n", ""); } if (on_list(cmd, cmds_ts)) { printf("%-24s= specifies the URL of the RFC 3161 Time-Stamp Authority server\n", "-ts"); - printf("%26sthis option cannot be used with the -t option\n", ""); + printf("%26sURLs are tried in order until one succeeds (unless -timestamp-all is used)\n", ""); + printf("%-24s= require a timestamp from every -t and -ts URL; allows both protocols\n", "-timestamp-all"); + printf("%26sat least one -t or -ts URL is required\n", ""); } if (on_list(cmd, cmds_time)) printf("%-24s= the unix-time to set the signing and/or verifying time\n", "-time"); @@ -4738,7 +4734,10 @@ static int main_configure(int argc, char **argv, GLOBAL_OPTIONS *options) options->https_cafile = get_cafile(); options->tsa_cafile = get_cafile(); } - for (argc--,argv++; argc >= 1; argc--,argv++) { + argc--; + argv++; + /* Options consume a variable number of arguments. */ + while (argc >= 1) { if (!strcmp(*argv, "-in")) { if (--argc < 1) { usage(argv0, "all"); @@ -4905,14 +4904,16 @@ static int main_configure(int argc, char **argv, GLOBAL_OPTIONS *options) return 0; /* FAILED */ } options->time = (time_t)strtoull(*(++argv), NULL, 10); + } else if ((cmd == CMD_SIGN || cmd == CMD_ADD) && !strcmp(*argv, "-timestamp-all")) { + options->timestamp_all = 1; } else if ((cmd == CMD_SIGN || cmd == CMD_ADD) && !strcmp(*argv, "-t")) { - if (--argc < 1) { + if (--argc < 1 || options->nturl >= MAX_TS_SERVERS) { usage(argv0, "all"); return 0; /* FAILED */ } options->turl[options->nturl++] = *(++argv); } else if ((cmd == CMD_SIGN || cmd == CMD_ADD) && !strcmp(*argv, "-ts")) { - if (--argc < 1) { + if (--argc < 1 || options->ntsurl >= MAX_TS_SERVERS) { usage(argv0, "all"); return 0; /* FAILED */ } @@ -5083,6 +5084,8 @@ static int main_configure(int argc, char **argv, GLOBAL_OPTIONS *options) failarg = *argv; break; } + argc--; + argv++; } if (!options->infile && argc > 0) { options->infile = *(argv++); @@ -5103,7 +5106,8 @@ static int main_configure(int argc, char **argv, GLOBAL_OPTIONS *options) return 0; /* FAILED */ } if (argc > 0 || - (options->nturl && options->ntsurl) || + (options->nturl && options->ntsurl && !options->timestamp_all) || + (options->timestamp_all && !options->nturl && !options->ntsurl) || (options->nturl && options->tsa_certfile && options->tsa_keyfile) || (options->ntsurl && options->tsa_certfile && options->tsa_keyfile) || !options->infile || diff --git a/osslsigncode.h b/osslsigncode.h index 2c47ad6..19e2ae0 100644 --- a/osslsigncode.h +++ b/osslsigncode.h @@ -289,6 +289,7 @@ typedef struct { int nturl; char *tsurl[MAX_TS_SERVERS]; int ntsurl; + int timestamp_all; char *proxy; int noverifypeer; int addBlob; diff --git a/osslsigncode.md b/osslsigncode.md index 4d923ec..cec3896 100644 --- a/osslsigncode.md +++ b/osslsigncode.md @@ -22,7 +22,7 @@ osslsigncode - Authenticode signing, timestamping, extraction, attachment, remov [`-h` *digest*] [`-n` *description*] [`-i` *URL*] [`-jp` `low`] [`-comm`] [`-ph`] -[`-t` *URL* ... | `-ts` *URL* ...] +[`-t` *URL* ...] [`-ts` *URL* ...] [`-timestamp-all`] [`-TSA-certs` *file* `-TSA-key` *file-or-URI* [`-TSA-time` *unix-time*]] [`-HTTPS-CAfile` *file*] [`-HTTPS-CRLfile` *file*] [`-time` *unix-time*] @@ -36,7 +36,7 @@ osslsigncode - Authenticode signing, timestamping, extraction, attachment, remov `osslsigncode` `add` [`-addUnauthenticatedBlob` [`-blobFile` *file*]] -[`-t` *URL* ... | `-ts` *URL* ...] +[`-t` *URL* ...] [`-ts` *URL* ...] [`-timestamp-all`] [`-TSA-certs` *file* `-TSA-key` *file-or-URI* [`-TSA-time` *unix-time*]] [`-HTTPS-CAfile` *file*] [`-HTTPS-CRLfile` *file*] [`-h` *digest*] [`-index` *n*] [`-verbose`] [`-add-msi-dse`] @@ -275,18 +275,28 @@ build. ## Timestamping and network options -The following timestamping modes are **mutually exclusive** within a single -`sign` or `add` invocation: +By default, Authenticode (`-t`) and RFC 3161 (`-ts`) timestamping are +mutually exclusive within one `sign` or `add` invocation. With +`-timestamp-all`, both protocols can be used together. Built-in timestamping +(`-TSA-certs` and `-TSA-key`) cannot be combined with remote timestamping. -- Authenticode timestamping with `-t` -- RFC 3161 timestamping with `-ts` -- built-in RFC 3161 timestamp generation with `-TSA-certs` and `-TSA-key` +Adding a timestamp preserves existing timestamps, including timestamps of the +other protocol. Multiple timestamps are stored as values of their respective +unsigned attributes. To replace timestamps, explicitly remove the signature +and sign again; `add` no longer replaces existing timestamps implicitly. `-t` *URL* -: Add an Authenticode timestamp from the specified URL. May be repeated. +: Add an Authenticode timestamp. May be repeated to specify fallback servers; + stop after the first successful response unless `-timestamp-all` is used. `-ts` *URL* -: Add an RFC 3161 timestamp from the specified URL. May be repeated. +: Add an RFC 3161 timestamp. May be repeated to specify fallback servers; + stop after the first successful response unless `-timestamp-all` is used. + +`-timestamp-all` +: Require a timestamp from every specified `-t` and `-ts` URL; at least one + URL is required. Allows both protocols in one invocation. If any request fails, the operation fails + rather than producing a partially timestamped output. `-p` *proxy* : Proxy used for timestamp or CRL retrieval. @@ -328,6 +338,18 @@ The following timestamping modes are **mutually exclusive** within a single ## Verification options +Every value of every Authenticode and RFC 3161 timestamp attribute is checked. +A signature with timestamps succeeds if at least one timestamp is trusted and +valid **and** the Authenticode signature validates at that timestamp's time. +Invalid, untrusted, or malformed alternatives do not override a successful +pair. If no pair validates, that signature fails, even if the signing +certificate is currently valid. A malformed or empty timestamp attribute is +not treated as an absent timestamp. + +Without timestamps, or with `-ignore-timestamp`, the requested `-time` or +current time is used instead. For a file containing multiple signatures, one +eligible valid signature still suffices; `-index` restricts which are tried. + `-c`, `-catalog` *file* : Verify the input file against the specified catalog file. @@ -389,7 +411,8 @@ missing TSA trust chain are supplied with `-TSA-CAfile`, and where needed `-TSA-CRLfile`. conflicting timestamp modes -: `-t`, `-ts`, and built-in TSA signing cannot be combined in one command. +: Combining `-t` and `-ts` requires `-timestamp-all`. Built-in TSA signing + cannot be combined with either remote timestamp mode. MSI signature mode mismatch : Re-signing or nesting an MSI signature must be consistent with whether the From cfb3de2aba6e0040b186efa8c3ff67a89aeeb564 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Trojnara?= Date: Fri, 6 Nov 2026 15:09:11 +0100 Subject: [PATCH 2/2] Reject truncated RFC 3161 message imprints verify_timestamp_token() compared the computed digest with the token's messageImprint using the imprint's own length. An empty imprint therefore matched any signature, and a short one bound only a prefix of the digest. A trusted TSA that signs such a request, even once, produces a token that can be attached to other signatures to claim an earlier signing time, for example to make a signature with an expired certificate verify. An imprint longer than the digest read past the computed value. Require the imprint length to equal the digest size before comparing. Also reject an unknown imprint algorithm explicitly and check the digest computation, whose failure was previously ignored. Authenticode timestamps are unaffected: CMS_verify() binds them through the messageDigest attribute. --- NEWS.md | 3 +++ osslsigncode.c | 35 ++++++++++++++--------------------- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/NEWS.md b/NEWS.md index 0b3b049..ba3a904 100644 --- a/NEWS.md +++ b/NEWS.md @@ -14,6 +14,9 @@ - fixed heap out-of-bounds reads when parsing malformed APPX ZIP64 extra fields - fixed heap out-of-bounds reads when verifying malformed PKCS#7 signature attributes - fixed undefined behavior when copying an empty MSI signature blob +- fixed RFC 3161 timestamp verification accepting tokens whose message + imprint is empty or shorter than its digest, which did not bind them to + the signature - added multiple timestamp support: `add` preserves existing timestamps and `-timestamp-all` requires every specified Authenticode/RFC 3161 server - verification now tries all timestamps and accepts any trusted timestamp for diff --git a/osslsigncode.c b/osslsigncode.c index 7919488..ef415c7 100644 --- a/osslsigncode.c +++ b/osslsigncode.c @@ -2200,12 +2200,11 @@ static int verify_timestamp_token(PKCS7 *p7, CMS_ContentInfo *timestamp) TS_TST_INFO *token = d2i_TS_TST_INFO(NULL, &p, len); if (token) { - BIO *bhash; u_char mdbuf[EVP_MAX_MD_SIZE]; ASN1_OCTET_STRING *hash; const ASN1_OBJECT *aoid; const u_char *hash_data; - int hash_len, md_nid; + int hash_len, md_len, md_nid; const EVP_MD *md; TS_MSG_IMPRINT *msg_imprint = TS_TST_INFO_get_msg_imprint(token); const X509_ALGOR *alg = TS_MSG_IMPRINT_get_algo(msg_imprint); @@ -2215,35 +2214,29 @@ static int verify_timestamp_token(PKCS7 *p7, CMS_ContentInfo *timestamp) X509_ALGOR_get0(&aoid, NULL, NULL, alg); md_nid = OBJ_obj2nid(aoid); md = EVP_get_digestbynid(md_nid); + md_len = md ? EVP_MD_size(md) : 0; /* compute a hash from the encrypted message digest value of the file */ - bhash = BIO_new(BIO_f_md()); -#if defined(__GNUC__) -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wcast-qual" -#endif - if (!BIO_set_md(bhash, md)) { - fprintf(stderr, "Unable to set the message digest of BIO\n"); - BIO_free_all(bhash); + if (md_len <= 0 || digest_len < 0 + || !EVP_Digest(digest, (size_t)digest_len, mdbuf, NULL, md, NULL)) { + fprintf(stderr, "Unable to compute the timestamp message imprint: %s\n", + (md_nid == NID_undef) ? "UNKNOWN" : OBJ_nid2ln(md_nid)); TS_TST_INFO_free(token); return 0; /* FAILED */ } -#if defined(__GNUC__) -#pragma GCC diagnostic pop -#endif - BIO_push(bhash, BIO_new(BIO_s_null())); - BIO_write(bhash, digest, digest_len); - BIO_gets(bhash, (char*)mdbuf, EVP_MD_size(md)); - BIO_free_all(bhash); - /* compare the provided hash against the computed hash */ - hash =TS_MSG_IMPRINT_get_msg(msg_imprint); + /* + * Compare the provided hash against the computed hash. + * A shorter imprint would only bind a prefix of the digest, + * and an empty one would match any signature. + */ + hash = TS_MSG_IMPRINT_get_msg(msg_imprint); hash_data = ASN1_STRING_get0_data(hash); hash_len = ASN1_STRING_length(hash); - if (memcmp(mdbuf, hash_data, (size_t)hash_len)) { + if (hash_len != md_len || memcmp(mdbuf, hash_data, (size_t)md_len)) { printf("Hash value mismatch:\n\tMessage digest algorithm: %s\n", (md_nid == NID_undef) ? "UNKNOWN" : OBJ_nid2ln(md_nid)); - print_hash("\tComputed message digest", "", mdbuf, EVP_MD_size(md)); + print_hash("\tComputed message digest", "", mdbuf, md_len); print_hash("\tReceived message digest", "", hash_data, hash_len); printf("\nFile's message digest verification: failed\n"); TS_TST_INFO_free(token);