From 4b7a5bdb7fd9a2b485546c838bbbbc29b92dedb0 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] 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 excess URLs before writing past the timestamp server arrays and use a while loop for variable-length argument consumption. Cover append behavior across all supported formats, mixed protocols, multiple attribute values, invalid alternatives, certificate validity windows, nested signatures, and all-or-fail timestamp requests with a self-contained loopback TSA. Document the new verification policy and the change from implicit timestamp replacement to appending. --- NEWS.md | 4 + README.md | 14 ++ cmake/CMakeTest.cmake | 10 + osslsigncode.c | 272 +++++++++++++-------------- osslsigncode.h | 1 + osslsigncode.md | 43 ++++- tests/multiple_timestamps.py | 349 +++++++++++++++++++++++++++++++++++ 7 files changed, 548 insertions(+), 145 deletions(-) create mode 100644 tests/multiple_timestamps.py diff --git a/NEWS.md b/NEWS.md index 711e6d94..ae84a572 100644 --- a/NEWS.md +++ b/NEWS.md @@ -5,6 +5,10 @@ - fixed Cygwin compilation by not passing ELF-specific linker flags - fixed MSI MsiDigitalSignatureEx digest ordering during signing and verification - fixed verification to report failure when timestamp verification fails +- 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 360cff91..14069203 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 547ba941..36f1e1cb 100644 --- a/cmake/CMakeTest.cmake +++ b/cmake/CMakeTest.cmake @@ -424,6 +424,16 @@ if(Python3_FOUND AND NOT cryptography_error) ### Sign with Time-Stamp Authority ### + # Self-contained loopback TSA and DER fixtures; checks exit status AND + # diagnostics for multiple, mixed, untrusted and malformed timestamps. + add_test(NAME "multiple_timestamps" + COMMAND ${Python3_EXECUTABLE} "${CMAKE_CURRENT_SOURCE_DIR}/tests/multiple_timestamps.py" + ${OSSLSIGNCODE} "${CMAKE_CURRENT_SOURCE_DIR}/tests/files/unsigned.exe") + set_tests_properties("multiple_timestamps" PROPERTIES + ENVIRONMENT "HTTP_PROXY=;http_proxy=;HTTPS_PROXY=;https_proxy=;ALL_PROXY=;all_proxy=" + TIMEOUT 120) + list(APPEND ALL_TESTS "multiple_timestamps") + # Sign with the RFC3161 Time-Stamp Authority set(pem_certs "cert" "expired" "revoked") foreach(ext ${extensions_all}) diff --git a/osslsigncode.c b/osslsigncode.c index 26a3eafb..0024722f 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)); @@ -2836,68 +2817,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) { @@ -2929,8 +2849,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; } /* @@ -3043,7 +2961,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, @@ -3058,7 +2976,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. */ @@ -3066,6 +2984,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; @@ -3095,6 +3014,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; } @@ -3213,6 +3133,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 @@ -3220,11 +3231,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) { @@ -3236,7 +3245,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"); @@ -3254,24 +3263,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 */ @@ -3706,8 +3700,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", ""); @@ -3729,8 +3723,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", ""); @@ -4003,11 +3997,12 @@ 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"); } if (on_list(cmd, cmds_time)) printf("%-24s= the unix-time to set the signing and/or verifying time\n", "-time"); @@ -4733,7 +4728,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"); @@ -4900,14 +4898,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 */ } @@ -5078,6 +5078,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++); @@ -5098,7 +5100,7 @@ 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->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 e218743d..5729abfa 100644 --- a/osslsigncode.h +++ b/osslsigncode.h @@ -288,6 +288,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 4d923ece..806f0155 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. 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 diff --git a/tests/multiple_timestamps.py b/tests/multiple_timestamps.py new file mode 100644 index 00000000..d2f0c733 --- /dev/null +++ b/tests/multiple_timestamps.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python3 +"""End-to-end timestamp tests, using only local keys and a loopback TSA. + +The small DER reader edits unsigned attributes only, allowing fixtures with +multiple values, repeated attributes, and malformed timestamp values. +""" + +import base64 +import copy +import datetime +import http.server +import pathlib +import struct +import subprocess +import sys +import tempfile +import threading + +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding, rsa +from cryptography.hazmat.primitives.serialization import pkcs7 +from cryptography.x509.oid import ExtendedKeyUsageOID, NameOID + + +class DER: + """Single-octet-tag DER node (sufficient for these PKCS#7 fixtures).""" + + def __init__(self, tag, value): + self.tag, self.value = tag, value + + def encode(self): + value = self.value + if isinstance(value, list): + value = b"".join(item.encode() for item in value) + size = len(value) + length = bytes([size]) if size < 128 else size.to_bytes((size.bit_length() + 7) // 8, "big") + if size >= 128: + length = bytes([128 + len(length)]) + length + return bytes([self.tag]) + length + value + + @classmethod + def parse(cls, data): + tag, size = data[:2] + offset = 2 + if size & 128: + count = size & 127 + size = int.from_bytes(data[offset:offset + count], "big") + offset += count + end = offset + size + assert end <= len(data) + value = data[offset:end] + if tag & 32: + children = [] + while value: + child, value = cls.parse(value) + children.append(child) + value = children + return cls(tag, value), data[end:] + + +def decode(data): + node, rest = DER.parse(data) + assert not rest + return node + + +def signed_data(node): + return node.value[1].value[0] + + +def signer_info(node): + return signed_data(node).value[-1].value[0] + + +RFC3161 = bytes.fromhex("2b060104018237030301") +COUNTERSIGNATURE = bytes.fromhex("2a864886f70d010906") +SIGNING_TIME = bytes.fromhex("2a864886f70d010905") + + +def timestamps(node): + si = signer_info(node) + if si.value[-1].tag != 0xA1: + return [] + return [attr for attr in si.value[-1].value + if attr.value[0].value in (RFC3161, COUNTERSIGNATURE)] + + +def run(*args, expected=0): + result = subprocess.run([str(arg) for arg in args], capture_output=True, text=True, check=False) + codes = (expected,) if isinstance(expected, int) else expected + assert result.returncode in codes, (args, result.returncode, result.stdout, result.stderr) + return result.stdout + result.stderr + + +def certificate(directory, name, eku, expires=2038): + key = rsa.generate_private_key(public_exponent=65537, key_size=2048) + subject = x509.Name([x509.NameAttribute(NameOID.COMMON_NAME, name)]) + cert = (x509.CertificateBuilder().subject_name(subject).issuer_name(subject) + .public_key(key.public_key()).serial_number(x509.random_serial_number()) + .not_valid_before(datetime.datetime(2018, 1, 1)) + .not_valid_after(datetime.datetime(expires, 1, 1)) + .add_extension(x509.BasicConstraints(ca=False, path_length=None), critical=True) + .add_extension(x509.KeyUsage(True, False, False, False, False, False, False, False, False), critical=True) + .add_extension(x509.ExtendedKeyUsage([eku]), critical=True) + .sign(key, hashes.SHA256())) + (directory / (name + ".pem")).write_bytes(cert.public_bytes(serialization.Encoding.PEM)) + (directory / (name + ".key")).write_bytes(key.private_bytes( + serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption())) + return key, cert + + +def main(): + exe, unsigned = pathlib.Path(sys.argv[1]).resolve(), pathlib.Path(sys.argv[2]).resolve() + with tempfile.TemporaryDirectory(prefix="multiple-timestamps-") as tmp: + directory = pathlib.Path(tmp) + certificate(directory, "code", ExtendedKeyUsageOID.CODE_SIGNING) + certificate(directory, "short", ExtendedKeyUsageOID.CODE_SIGNING, 2020) + tsa_key, tsa_cert = certificate(directory, "tsa", ExtendedKeyUsageOID.TIME_STAMPING) + certificate(directory, "other", ExtendedKeyUsageOID.TIME_STAMPING) + sequence = 0 + + def output(suffix=".exe"): + nonlocal sequence + sequence += 1 + return directory / (str(sequence) + suffix) + + def sign(name="code", *extra, source=unsigned): + dest = output(source.suffix) + run(exe, "sign", "-certs", directory / (name + ".pem"), + "-key", directory / (name + ".key"), *extra, "-in", source, "-out", dest) + return dest + + def add(source, *extra, expected=0): + dest = output(source.suffix) + run(exe, "add", *extra, "-in", source, "-out", dest, expected=expected) + if expected != 0: + assert not dest.exists(), "Failed timestamping left a partial output" + return dest + + def builtin(name="tsa", time="1556668800"): + return ("-TSA-certs", directory / (name + ".pem"), + "-TSA-key", directory / (name + ".key"), "-TSA-time", time) + + def extract(source): + dest = output(".der") + run(exe, "extract-signature", "-in", source, "-out", dest) + return decode(dest.read_bytes()) + + def verify(source, expected=0, ignore=False, ca="code", successes=None, failures=None): + text = run(exe, "verify", "-CAfile", directory / (ca + ".pem"), + "-TSA-CAfile", directory / "tsa.pem", "-time", "1567296000", + *(["-ignore-timestamp"] if ignore else []), + "-in", source, expected=expected) + if successes is not None: + assert text.count("Timestamp Server Signature verification: ok") == successes, text + if failures is not None: + assert text.count("Timestamp Server Signature verification: failed") == failures, text + return text + + plain = sign() + verify(plain) + first = add(plain, *builtin()) + second = add(first, *builtin(time="1567296000")) + attrs = timestamps(extract(second)) + assert len(attrs) == 1 and len(attrs[0].value[1].value) == 2 + verify(second, successes=2) + for source in sorted(unsigned.parent.glob("unsigned.*")): + if source != unsigned: + stamped = sign("code", *builtin(), source=source) + verify(add(stamped, *builtin(time="1567296000")), successes=2) + + # Untrusted timestamps never mask a trusted one, regardless of add order. + bad = add(plain, *builtin("other")) + verify(bad, expected=1, failures=1) + verify(bad, ignore=True) + verify(add(first, *builtin("other")), successes=1, failures=1) + verify(add(bad, *builtin()), successes=1, failures=1) + verify(add(bad, *builtin("other", "1567296000")), expected=1, failures=2) + nested = output() + run(exe, "sign", "-nest", "-certs", directory / "code.pem", + "-key", directory / "code.key", "-in", bad, "-out", nested) + verify(nested, failures=1) # A separate valid signature still suffices. + nested = add(nested, "-index", "1", *builtin()) + verify(add(nested, "-index", "1", *builtin(time="1567296000")), successes=2, failures=1) + + # A trusted TSA is insufficient: the code certificate must be valid at + # that particular timestamp. Try alternatives, not just the first TSA. + short = sign("short") + late = add(short, *builtin(time="1609459200")) + verify(late, expected=1, ca="short", successes=1) + verify(add(late, *builtin()), ca="short", successes=2) + early = add(short, *builtin()) + verify(add(early, *builtin(time="1609459200")), ca="short", successes=2) + + # Directly write PE certificate tables to avoid attach-signature's + # verification policy interfering with deliberately invalid fixtures. + def fixture(node): + data = bytearray(plain.read_bytes()) + pe = struct.unpack_from("