From 5b1c9bda8658ff2c3d7ddc400bc9562201b44949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Trojnara?= Date: Fri, 6 Nov 2026 18:32:57 +0100 Subject: [PATCH 1/4] Pair each timestamp with its own time time_t_timestamp_get_attributes() keeps the last timestamp attribute that decodes, but it updated the returned time before deciding whether to keep the attribute. With more than one timestamp attribute, the time used for verification could therefore come from a different attribute than the timestamp that was verified. Take the time only together with the timestamp it belongs to, and free a timestamp replaced by a later one instead of leaking it. Also free the countersignature when cms_get_timestamp() fails before taking ownership of it. Which timestamp is verified is unchanged. --- NEWS.md | 2 ++ osslsigncode.c | 27 +++++++++++++++++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/NEWS.md b/NEWS.md index 454f017..5ddf4d1 100644 --- a/NEWS.md +++ b/NEWS.md @@ -14,6 +14,8 @@ - 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 timestamp verification of signatures with more than one timestamp + attribute ### 2.14 (2026.07.20) diff --git a/osslsigncode.c b/osslsigncode.c index 320e930..0505317 100644 --- a/osslsigncode.c +++ b/osslsigncode.c @@ -2845,6 +2845,7 @@ static time_t time_t_timestamp_get_attributes(CMS_ContentInfo **timestamp, PKCS7 /* Authenticode Timestamp - Policy OID: 1.2.840.113549.1.9.6 */ CMS_ContentInfo *cms; PKCS7_SIGNER_INFO *countersi; + time_t ts_time; value = (const ASN1_STRING *)X509_ATTRIBUTE_get0_data(attr, 0, V_ASN1_SEQUENCE, NULL); if (value == NULL) @@ -2856,17 +2857,21 @@ static time_t time_t_timestamp_get_attributes(CMS_ContentInfo **timestamp, PKCS7 ERR_print_errors_fp(stderr); continue; } - time = time_t_get_si_time(countersi); - if (time != INVALID_TIME) { + ts_time = time_t_get_si_time(countersi); + if (ts_time != INVALID_TIME) { + /* cms_get_timestamp() takes ownership of countersi */ cms = cms_get_timestamp(p7->d.sign, countersi); if (cms) { - if (!print_cms_timestamp(cms, time)) { + if (!print_cms_timestamp(cms, ts_time)) { CMS_ContentInfo_free(cms); printf("Warning: Authenticode Timestamp could not be decoded correctly\n"); ERR_print_errors_fp(stderr); continue; } + /* Only take the time together with the timestamp it came from */ + CMS_ContentInfo_free(*timestamp); *timestamp = cms; + time = ts_time; } else { printf("Warning: Corrupt Authenticode Timestamp embedded content\n"); } @@ -2877,6 +2882,7 @@ static time_t time_t_timestamp_get_attributes(CMS_ContentInfo **timestamp, PKCS7 } 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; + time_t ts_time; value = (const ASN1_STRING *)X509_ATTRIBUTE_get0_data(attr, 0, V_ASN1_SEQUENCE, NULL); if (value == NULL) @@ -2888,15 +2894,18 @@ static time_t time_t_timestamp_get_attributes(CMS_ContentInfo **timestamp, PKCS7 ERR_print_errors_fp(stderr); continue; } - time = time_t_get_cms_time(cms); - if (time != INVALID_TIME) { - if (!print_cms_timestamp(cms, time)) { + ts_time = time_t_get_cms_time(cms); + if (ts_time != INVALID_TIME) { + if (!print_cms_timestamp(cms, ts_time)) { CMS_ContentInfo_free(cms); printf("Warning: RFC3161 Timestamp could not be decoded correctly\n"); ERR_print_errors_fp(stderr); continue; } + /* Only take the time together with the timestamp it came from */ + CMS_ContentInfo_free(*timestamp); *timestamp = cms; + time = ts_time; } else { printf("Warning: Corrupt RFC3161 Timestamp embedded content\n"); CMS_ContentInfo_free(cms); @@ -3048,7 +3057,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 +3072,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 +3080,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 +3110,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; } From 146c94219d844249376a6b0019b7da33717ff157 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/4] Reject truncated RFC 3161 message imprints verify_timestamp_token() compared the computed digest with the token's messageImprint using the imprint's own length, so an empty or shortened imprint was accepted, and a longer one 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 | 2 ++ osslsigncode.c | 35 ++++++++++++++--------------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/NEWS.md b/NEWS.md index 5ddf4d1..bb50da8 100644 --- a/NEWS.md +++ b/NEWS.md @@ -16,6 +16,8 @@ - fixed undefined behavior when copying an empty MSI signature blob - fixed timestamp verification of signatures with more than one timestamp attribute +- fixed RFC 3161 timestamp verification to require a full-length message + imprint ### 2.14 (2026.07.20) diff --git a/osslsigncode.c b/osslsigncode.c index 0505317..c9a1f93 100644 --- a/osslsigncode.c +++ b/osslsigncode.c @@ -2215,12 +2215,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); @@ -2230,35 +2229,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); From 7fdab98422174a773e3b8bfb48897ecff842178e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Trojnara?= Date: Fri, 6 Nov 2026 18:33:59 +0100 Subject: [PATCH 3/4] Limit the number of timestamp server URLs Each "-t" and "-ts" option was stored in a fixed array of MAX_TS_SERVERS (256) entries without a bounds check, so a longer command line wrote past the arrays in GLOBAL_OPTIONS and crashed. Reject further URLs once the limit is reached. --- NEWS.md | 1 + osslsigncode.c | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/NEWS.md b/NEWS.md index bb50da8..976a07a 100644 --- a/NEWS.md +++ b/NEWS.md @@ -18,6 +18,7 @@ attribute - fixed RFC 3161 timestamp verification to require a full-length message imprint +- fixed a crash with more than 256 "-t" or "-ts" options ### 2.14 (2026.07.20) diff --git a/osslsigncode.c b/osslsigncode.c index c9a1f93..d505d2b 100644 --- a/osslsigncode.c +++ b/osslsigncode.c @@ -4914,12 +4914,20 @@ static int main_configure(int argc, char **argv, GLOBAL_OPTIONS *options) usage(argv0, "all"); return 0; /* FAILED */ } + if (options->nturl >= MAX_TS_SERVERS) { + fprintf(stderr, "Too many -t options (maximum %d)\n", MAX_TS_SERVERS); + return 0; /* FAILED */ + } options->turl[options->nturl++] = *(++argv); } else if ((cmd == CMD_SIGN || cmd == CMD_ADD) && !strcmp(*argv, "-ts")) { if (--argc < 1) { usage(argv0, "all"); return 0; /* FAILED */ } + if (options->ntsurl >= MAX_TS_SERVERS) { + fprintf(stderr, "Too many -ts options (maximum %d)\n", MAX_TS_SERVERS); + return 0; /* FAILED */ + } options->tsurl[options->ntsurl++] = *(++argv); } else if ((cmd == CMD_SIGN || cmd == CMD_ADD || cmd == CMD_VERIFY) && !strcmp(*argv, "-p")) { if (--argc < 1) { From 22535b2cbefcff3a8c19cc66840874a466fff4aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Trojnara?= Date: Fri, 6 Nov 2026 18:38:32 +0100 Subject: [PATCH 4/4] Test verification of crafted timestamp tokens The test TSA only produces correct tokens, so neither the message imprint check nor signatures with more than one timestamp attribute were covered. Generate RFC 3161 tokens with the test TSA key instead, on a signature whose certificate is valid only during 2018: a valid 2018 token, which must verify, and tokens that must not: a valid 2030 token, empty and 1-byte message imprints, and the 2030 token followed by a second, unverifiable timestamp attribute. The helper builds the DER itself, so it needs only the cryptography module, and it does not use the timestamping server. --- cmake/CMakeTest.cmake | 38 +++++ tests/make_crafted_timestamps.py | 281 +++++++++++++++++++++++++++++++ 2 files changed, 319 insertions(+) create mode 100644 tests/make_crafted_timestamps.py diff --git a/cmake/CMakeTest.cmake b/cmake/CMakeTest.cmake index 6b7a6f9..cdfc462 100644 --- a/cmake/CMakeTest.cmake +++ b/cmake/CMakeTest.cmake @@ -45,6 +45,7 @@ if(Python3_FOUND) "${CMAKE_CURRENT_SOURCE_DIR}/tests/client_http.py" "${CMAKE_CURRENT_SOURCE_DIR}/tests/make_certificates.py" "${CMAKE_CURRENT_SOURCE_DIR}/tests/make_large_pe.py" + "${CMAKE_CURRENT_SOURCE_DIR}/tests/make_crafted_timestamps.py" "${CMAKE_CURRENT_SOURCE_DIR}/tests/check_pe_checksum.py" "${CMAKE_CURRENT_SOURCE_DIR}/tests/start_server.py" "${CMAKE_CURRENT_SOURCE_DIR}/tests/exec.py" @@ -696,6 +697,43 @@ if(Python3_FOUND AND NOT cryptography_error) message(STATUS "CTest skips some tests") endif(OPENSSL_VERSION VERSION_GREATER_EQUAL "3.0.0" OR CURL_FOUND) +### Verify crafted timestamps ### + + # The signer certificate is valid only during 2018; tokens are signed with + # the test TSA key and need no timestamping server + add_test(NAME "make_crafted_timestamps" + COMMAND ${Python3_EXECUTABLE} "${TEST_DIR}/make_crafted_timestamps.py" + "--osslsigncode" ${OSSLSIGNCODE} + "--certs" "${CERTS}" + "--in" "${FILES}/unsigned.exe" + "--out" "${FILES}") + set_tests_properties("make_crafted_timestamps" PROPERTIES + DEPENDS "start_server") + list(APPEND ALL_TESTS "make_crafted_timestamps") + + # ok: a valid 2018 timestamp; the others are expected to fail: + # late: a valid 2030 timestamp, when the certificate had expired + # empty, short: an empty or 1-byte message imprint + # two_attrs: the 2030 timestamp and a second, unverifiable timestamp attribute + foreach(crafted "ok" "late" "empty" "short" "two_attrs") + add_test(NAME "verify_crafted_ts_${crafted}" + COMMAND ${Python3_EXECUTABLE} ${EXEC} ${OSSLSIGNCODE} "verify" + "-CAfile" "${CERTS}/CACert.pem" + "-CRLfile" "${CERTS}/CACertCRL.pem" + "-TSA-CAfile" "${CERTS}/TSACA.pem" + "-TSA-CRLfile" "${CERTS}/TSACertCRL.pem" + "-ignore-cdp" + "-in" "${FILES}/ts_crafted_${crafted}.exe") + set_tests_properties("verify_crafted_ts_${crafted}" PROPERTIES + DEPENDS "make_crafted_timestamps") + if(NOT crafted STREQUAL "ok") + set_tests_properties("verify_crafted_ts_${crafted}" PROPERTIES + WILL_FAIL TRUE) + endif(NOT crafted STREQUAL "ok") + list(APPEND ALL_TESTS "verify_crafted_ts_${crafted}") + set(OUTPUT_FILES ${OUTPUT_FILES} "${FILES}/ts_crafted_${crafted}.exe") + endforeach(crafted "ok" "late" "empty" "short" "two_attrs") + # Delete test files set(names "signed" "nested" "revoked" "removed" "added") foreach(ext ${extensions_all}) diff --git a/tests/make_crafted_timestamps.py b/tests/make_crafted_timestamps.py new file mode 100644 index 0000000..dc3bc4d --- /dev/null +++ b/tests/make_crafted_timestamps.py @@ -0,0 +1,281 @@ +#!/usr/bin/python3 +"""Make PE files with crafted RFC 3161 timestamps for verification tests. + +The signature uses the expired test certificate, valid only during 2018, so +it verifies only with a valid timestamp from 2018. Timestamp tokens are +signed with the test TSA key; DER is built here so that only the +cryptography module is needed. +""" + +import argparse +import datetime +import hashlib +import os +import subprocess +import sys + +from cryptography import x509 +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import padding + +OID_SIGNED_DATA = "1.2.840.113549.1.7.2" +OID_CONTENT_TYPE = "1.2.840.113549.1.9.3" +OID_MESSAGE_DIGEST = "1.2.840.113549.1.9.4" +OID_TST_INFO = "1.2.840.113549.1.9.16.1.4" +OID_SHA256 = "2.16.840.1.101.3.4.2.1" +OID_RSA = "1.2.840.113549.1.1.1" +OID_RFC3161 = "1.3.6.1.4.1.311.3.3.1" +OID_POLICY = "1.3.6.1.4.1.42921.1.2.2" +SIGNING_TIME = datetime.datetime(2018, 6, 1, tzinfo=datetime.timezone.utc) +LATE_TIME = datetime.datetime(2030, 1, 1, tzinfo=datetime.timezone.utc) +SKI_MARKER = b"\x5a" * 20 + + +# Minimal DER encoding + +def der_len(length): + """DER length octets""" + if length < 0x80: + return bytes([length]) + body = length.to_bytes((length.bit_length() + 7) // 8, "big") + return bytes([0x80 | len(body)]) + body + + +def tlv(tag, body): + """Tag, length and value""" + return bytes([tag]) + der_len(len(body)) + body + + +def seq(*items): + """SEQUENCE of encoded items""" + return tlv(0x30, b"".join(items)) + + +def set_of(*items): + """SET OF encoded items in DER order""" + return tlv(0x31, b"".join(sorted(items))) + + +def integer(value): + """INTEGER""" + return tlv(0x02, value.to_bytes(value.bit_length() // 8 + 1, "big", signed=True)) + + +def oid(dotted): + """OBJECT IDENTIFIER from dotted notation""" + arcs = [int(arc) for arc in dotted.split(".")] + body = bytes([40 * arcs[0] + arcs[1]]) + for arc in arcs[2:]: + chunk = [arc & 0x7f] + arc >>= 7 + while arc: + chunk.insert(0, 0x80 | (arc & 0x7f)) + arc >>= 7 + body += bytes(chunk) + return tlv(0x06, body) + + +def octets(data): + """OCTET STRING""" + return tlv(0x04, data) + + +def algorithm(dotted): + """AlgorithmIdentifier with NULL parameters""" + return seq(oid(dotted), b"\x05\x00") + + +def generalized_time(when): + """GeneralizedTime in UTC""" + return tlv(0x18, when.strftime("%Y%m%d%H%M%SZ").encode()) + + +# Minimal DER parsing + +def parse(data, pos=0): + """Return (tag, start, content_start, end) of the element at pos.""" + tag = data[pos] + length = data[pos + 1] + content = pos + 2 + if length & 0x80: + count = length & 0x7f + length = int.from_bytes(data[content:content + count], "big") + content += count + return tag, pos, content, content + length + + +def children(data, element): + """Child elements of a constructed element""" + _, _, pos, end = element + result = [] + while pos < end: + child = parse(data, pos) + result.append(child) + pos = child[3] + return result + + +def raw(data, element): + """Complete encoding of an element""" + return data[element[1]:element[3]] + + +def signer_info_path(sig): + """Return the ContentInfo, SignedData and first SignerInfo elements.""" + content_info = parse(sig) + signed_data = parse(sig, children(sig, children(sig, content_info)[1])[0][1]) + signer_infos = children(sig, signed_data)[-1] + return content_info, signed_data, signer_infos, children(sig, signer_infos)[0] + + +def encrypted_digest(sig): + """encryptedDigest of the first SignerInfo""" + signer_info = signer_info_path(sig)[3] + value = next(c for c in children(sig, signer_info) if c[0] == 0x04) + return sig[value[2]:value[3]] + + +def with_unsigned_attributes(sig, attributes): + """Replace the unsigned attributes of the first SignerInfo.""" + content_info, signed_data, signer_infos, signer_info = signer_info_path(sig) + fields = [raw(sig, c) for c in children(sig, signer_info) if c[0] != 0xa1] + new_signer_info = seq(*fields, tlv(0xa1, b"".join(sorted(attributes)))) + others = [raw(sig, c) for c in children(sig, signer_infos)[1:]] + new_signer_infos = tlv(0x31, new_signer_info + b"".join(others)) + sd_fields = [raw(sig, c) for c in children(sig, signed_data)[:-1]] + new_signed_data = seq(*sd_fields, new_signer_infos) + return seq(raw(sig, children(sig, content_info)[0]), tlv(0xa0, new_signed_data)) + + +# Timestamp tokens + +def tst_info(imprint, when, serial): + """TSTInfo with a SHA-256 message imprint""" + return seq(integer(1), oid(OID_POLICY), + seq(algorithm(OID_SHA256), octets(imprint)), + integer(serial), generalized_time(when)) + + +def token(tst, certs, signer_info): + """Timestamp token: SignedData with TSTInfo content""" + signed_data = seq( + integer(3), + set_of(algorithm(OID_SHA256)), + seq(oid(OID_TST_INFO), tlv(0xa0, octets(tst))), + tlv(0xa0, b"".join(sorted(c.public_bytes(serialization.Encoding.DER) for c in certs))), + set_of(signer_info)) + return seq(oid(OID_SIGNED_DATA), tlv(0xa0, signed_data)) + + +def signed_token(tst, tsa_cert, tsa_key, chain): + """Token signed by the TSA; also returns its signature""" + attrs = [seq(oid(OID_CONTENT_TYPE), set_of(oid(OID_TST_INFO))), + seq(oid(OID_MESSAGE_DIGEST), set_of(octets(hashlib.sha256(tst).digest())))] + attrs_body = b"".join(sorted(attrs)) + signature = tsa_key.sign(tlv(0x31, attrs_body), padding.PKCS1v15(), hashes.SHA256()) + signer_info = seq( + integer(1), + seq(tsa_cert.issuer.public_bytes(), integer(tsa_cert.serial_number)), + algorithm(OID_SHA256), + tlv(0xa0, attrs_body), + algorithm(OID_RSA), + octets(signature)) + return token(tst, chain, signer_info), signature + + +def unsigned_token(tst, padding_certs): + """Unverifiable token: subjectKeyIdentifier signer, dummy signature.""" + signer_info = seq(integer(3), tlv(0x80, SKI_MARKER), algorithm(OID_SHA256), + algorithm(OID_RSA), octets(b"\0" * 256)) + return token(tst, padding_certs, signer_info) + + +def rfc3161_attribute(*tokens): + """Unsigned attribute with RFC 3161 timestamp tokens""" + return seq(oid(OID_RFC3161), set_of(*tokens)) + + +def load_certs(path): + """Certificates from a PEM file""" + with open(path, "rb") as file: + data = file.read() + marker = b"-----END CERTIFICATE-----" + blocks = [block + marker for block in data.split(marker) if b"-----BEGIN" in block] + return [x509.load_pem_x509_certificate(block) for block in blocks] + + +def run(*args): + """Run a command and exit if it fails""" + proc = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + universal_newlines=True, check=False) + if proc.returncode: + print(proc.stdout) + sys.exit("Failed: " + " ".join(args)) + + +def main(): # pylint: disable=too-many-locals + """Make the crafted timestamp test files""" + parser = argparse.ArgumentParser() + parser.add_argument("--osslsigncode", required=True) + parser.add_argument("--certs", required=True) + parser.add_argument("--in", dest="infile", required=True) + parser.add_argument("--out", required=True, help="output directory") + args = parser.parse_args() + + tsa_cert = load_certs(os.path.join(args.certs, "TSA.pem"))[0] + chain = load_certs(os.path.join(args.certs, "tsa-chain.pem")) + with open(os.path.join(args.certs, "TSA.key"), "rb") as file: + tsa_key = serialization.load_pem_private_key(file.read(), password=None) + + def out(name): + path = os.path.join(args.out, name) + if os.path.exists(path): + os.remove(path) + return path + + signed = out("ts_crafted_signed.exe") + sig_file = out("ts_crafted_signed.der") + run(args.osslsigncode, "sign", + "-certs", os.path.join(args.certs, "expired.pem"), + "-key", os.path.join(args.certs, "key.pem"), + "-ac", os.path.join(args.certs, "CAcross.pem"), + "-h", "sha256", "-time", str(int(SIGNING_TIME.timestamp())), + "-in", args.infile, "-out", signed) + run(args.osslsigncode, "extract-signature", "-in", signed, "-out", sig_file) + with open(sig_file, "rb") as file: + sig = file.read() + imprint = hashlib.sha256(encrypted_digest(sig)).digest() + + good, _ = signed_token(tst_info(imprint, SIGNING_TIME, 1), tsa_cert, tsa_key, chain) + late, late_signature = signed_token(tst_info(imprint, LATE_TIME, 2), tsa_cert, tsa_key, chain) + empty, _ = signed_token(tst_info(b"", SIGNING_TIME, 3), tsa_cert, tsa_key, chain) + short, _ = signed_token(tst_info(imprint[:1], SIGNING_TIME, 4), tsa_cert, tsa_key, chain) + # Padded so that DER stores it after the late token + unverifiable = unsigned_token(tst_info(imprint, SIGNING_TIME, 5), chain * 8) + + cases = { + "ts_crafted_ok.exe": [rfc3161_attribute(good)], + "ts_crafted_late.exe": [rfc3161_attribute(late)], + "ts_crafted_empty.exe": [rfc3161_attribute(empty)], + "ts_crafted_short.exe": [rfc3161_attribute(short)], + "ts_crafted_two_attrs.exe": [rfc3161_attribute(late), rfc3161_attribute(unverifiable)], + } + for name, attributes in cases.items(): + der = out(name + ".der") + with open(der, "wb") as file: + file.write(with_unsigned_attributes(sig, attributes)) + run(args.osslsigncode, "attach-signature", "-sigin", der, + "-in", args.infile, "-out", out(name)) + os.remove(der) + + # The two-attribute case needs the unverifiable attribute stored second + with open(os.path.join(args.out, "ts_crafted_two_attrs.exe"), "rb") as file: + attached = file.read() + if not 0 <= attached.find(late_signature) < attached.find(SKI_MARKER): + sys.exit("ts_crafted_two_attrs.exe: unexpected timestamp order") + os.remove(signed) + os.remove(sig_file) + + +if __name__ == "__main__": + main()