async def sign_authenticode_file(context, orig_path, fmt, *, authenticode_comment=None): """Sign a file in-place with authenticode, using autograph as a backend. Args: context (Context): the signing context orig_path (str): the source file to sign fmt (str): the format to sign with comment (str): The authenticode comment to sign with, if present. currently only used for msi files. (Defaults to None) Returns: True on success, False otherwise """ if winsign.osslsigncode.is_signed(orig_path): log.info("%s is already signed", orig_path) return True fmt, keyid = utils.split_autograph_format(fmt) async def signer(digest, digest_algo): try: return await sign_hash_with_autograph(context, digest, fmt, keyid) except Exception: log.exception("Error signing authenticode hash with autograph") raise infile = orig_path outfile = orig_path + "-new" if "authenticode_ev" in fmt: digest_algo = "sha256" else: digest_algo = "sha1" if keyid: certs = load_pem_certs(open(context.config[f"authenticode_cert_{keyid}"], "rb").read()) else: certs = load_pem_certs(open(context.config["authenticode_cert"], "rb").read()) url = context.config["authenticode_url"] timestamp_style = context.config["authenticode_timestamp_style"] if fmt.endswith("authenticode_stub"): crosscert = context.config["authenticode_cross_cert"] else: crosscert = None if authenticode_comment and orig_path.endswith(".msi"): log.info("Using comment '%s' to sign %s", authenticode_comment, orig_path) elif authenticode_comment: log.info("Not using specified comment to sign %s, not yet implemented for non *.msi files.", orig_path) authenticode_comment = None if not await winsign.sign.sign_file( infile, outfile, digest_algo, certs, signer, url=url, comment=authenticode_comment, crosscert=crosscert, timestamp_style=timestamp_style, ): raise IOError(f"Couldn't sign {orig_path}") os.rename(outfile, infile) return True
def test_timestamp_old(test_file, digest_algo, tmp_path, signing_keys, httpserver): """Verify that we can sign with old style timestamps.""" signed_exe = tmp_path / "signed.exe" priv_key = load_private_key(open(signing_keys[0], "rb").read()) certs = load_pem_certs(signing_keys[1].read_bytes()) def signer(digest, digest_algo): return sign_signer_digest(priv_key, digest_algo, digest) httpserver.serve_content( (DATA_DIR / f"unsigned-{digest_algo}-ts-old.dat").read_bytes()) assert sign_file( test_file, signed_exe, digest_algo, certs, signer, timestamp_style="old", # Comment this out to use a real timestamp server so that we can # capture a response timestamp_url=httpserver.url, ) # Check that we have 3 certificates in the signature if is_pefile(test_file): with signed_exe.open("rb") as f: certificates = get_certificates(f) sigs = get_signatures_from_certificates(certificates) assert len(certificates) == 1 assert len(sigs) == 1 assert len(sigs[0]["certificates"]) == 3 assert verify_pefile(f)
def test_sign_file_dummy(tmp_path, signing_keys): """Check that we can sign with an additional dummy certificate. The extra dummy certs are used by the stub installer. """ test_file = DATA_DIR / "unsigned.exe" signed_exe = tmp_path / "signed.exe" priv_key = load_private_key(open(signing_keys[0], "rb").read()) certs = load_pem_certs(signing_keys[1].read_bytes()) def signer(digest, digest_algo): return sign_signer_digest(priv_key, digest_algo, digest) assert sign_file(test_file, signed_exe, "sha1", certs, signer, crosscert=signing_keys[1]) # Check that we have 2 certificates in the signature with signed_exe.open("rb") as f: certificates = get_certificates(f) sigs = get_signatures_from_certificates(certificates) assert len(certificates) == 1 assert len(sigs) == 1 assert len(sigs[0]["certificates"]) == 2
async def async_main(argv=None): """Main CLI entry point for signing (async).""" parser = build_parser() args = parser.parse_args(argv) logging.basicConfig(format="%(asctime)s - %(message)s", level=args.loglevel) if not args.priv_key: if not (args.autograph_user and args.autograph_secret): parser.error( "--key, or all of --autograph-url, --autograph-user, and " "--autograph-secret must be specified") if not args.outfile: args.outfile = args.infile certs = [] certs_data = open(args.certs, "rb").read() certs = load_pem_certs(certs_data) priv_key = load_private_key(open(args.priv_key, "rb").read()) signer = key_signer(priv_key) with tempfile.TemporaryDirectory() as d: d = Path(d) if args.infile == "-": args.infile = d / "unsigned" with args.infile.open("wb") as f: _copy_stream(sys.stdin.buffer, f) else: args.infile = Path(args.infile) if args.outfile == "-": outfile = d / "signed" else: outfile = Path(args.outfile) r = await sign_file( args.infile, outfile, args.digest_algo, certs, signer, url=args.url, comment=args.comment, timestamp_style=args.timestamp, ) # TODO: Extra cross-cert # TODO: Check with a cert chain if not r: return 1 if args.outfile == "-": with outfile.open("rb") as f: _copy_stream(f, sys.stdout.buffer) return 0
def test_sign_file_badfile(tmp_path, signing_keys): """Verify that we can't sign non-exe files.""" test_file = Path(__file__) signed_file = tmp_path / "signed.py" priv_key = load_private_key(open(signing_keys[0], "rb").read()) certs = load_pem_certs(signing_keys[1].read_bytes()) def signer(digest, digest_algo): return sign_signer_digest(priv_key, digest_algo, digest) assert not sign_file(test_file, signed_file, "sha1", certs, signer)
async def test_sign(test_file, digest_algo, tmp_path, signing_keys): """Check that we can sign a PE file.""" signed_exe = tmp_path / "signed.exe" priv_key = load_private_key(open(signing_keys[0], "rb").read()) certs = load_pem_certs(signing_keys[1].read_bytes()) # TODO: Make sure multiple works cert = certs[0] async def signer(digest, digest_algo): return sign_signer_digest(priv_key, digest_algo, digest) with test_file.open("rb") as infile, signed_exe.open("wb+") as outfile: assert await sign_file(infile, outfile, digest_algo, cert, signer) assert verify_pefile(outfile)
def test_sign_file_twocerts(tmp_path, signing_keys): """Check that we can include multiple certificates.""" test_file = DATA_DIR / "unsigned.exe" signed_exe = tmp_path / "signed.exe" priv_key = load_private_key(open(signing_keys[0], "rb").read()) certs = load_pem_certs(open(DATA_DIR / "twocerts.pem", "rb").read()) def signer(digest, digest_algo): return sign_signer_digest(priv_key, digest_algo, digest) assert sign_file(test_file, signed_exe, "sha1", certs, signer) # Check that we have 2 certificates in the signature with signed_exe.open("rb") as f: certificates = get_certificates(f) sigs = get_signatures_from_certificates(certificates) assert len(certificates) == 1 assert len(sigs) == 1 assert len(sigs[0]["certificates"]) == 2
def test_timestamp_rfc3161(test_file, digest_algo, tmp_path, signing_keys, httpserver): """Verify that we can sign with RFC3161 timestamps.""" signed_exe = tmp_path / "signed.exe" priv_key = load_private_key(open(signing_keys[0], "rb").read()) certs = load_pem_certs(signing_keys[1].read_bytes()) def signer(digest, digest_algo): return sign_signer_digest(priv_key, digest_algo, digest) httpserver.serve_content( (DATA_DIR / f"unsigned-{digest_algo}-ts-rfc3161.dat").read_bytes()) assert sign_file( test_file, signed_exe, digest_algo, certs, signer, timestamp_style="rfc3161", # Comment this out to use a real timestamp server so that we can # capture a response timestamp_url=httpserver.url, ) # Check that we have 1 certificate in the signature, # and have a counterSignature section if is_pefile(test_file): with signed_exe.open("rb") as f: certificates = get_certificates(f) sigs = get_signatures_from_certificates(certificates) assert len(certificates) == 1 assert len(sigs) == 1 assert len(sigs[0]["certificates"]) == 1 assert any((sigs[0]["signerInfos"][0]["unauthenticatedAttributes"] [i]["type"] == id_timestampSignature) for i in range( len(sigs[0]["signerInfos"][0] ["unauthenticatedAttributes"]))) assert verify_pefile(f)
async def sign_authenticode_file(context, orig_path, fmt): """Sign a file in-place with authenticode, using autograph as a backend. Args: context (Context): the signing context orig_path (str): the source file to sign fmt (str): the format to sign with Returns: True on success, False otherwise """ if winsign.osslsigncode.is_signed(orig_path): log.info("%s is already signed", orig_path) return True async def signer(digest, digest_algo): try: return await sign_hash_with_autograph(context, digest, fmt) except Exception: log.exception("Error signing authenticode hash with autograph") raise infile = orig_path outfile = orig_path + "-new" digest_algo = "sha1" certs = load_pem_certs(open(context.config["authenticode_cert"], "rb").read()) url = context.config["authenticode_url"] timestamp_style = context.config["authenticode_timestamp_style"] if fmt.endswith("authenticode_stub"): crosscert = context.config["authenticode_cross_cert"] else: crosscert = None if not await winsign.sign.sign_file(infile, outfile, digest_algo, certs, signer, url=url, crosscert=crosscert, timestamp_style=timestamp_style): raise IOError(f"Couldn't sign {orig_path}") os.rename(outfile, infile) return True
def test_sign_file(test_file, digest_algo, tmp_path, signing_keys): """Check that we can sign with the osslsign wrapper.""" signed_exe = tmp_path / "signed.exe" priv_key = load_private_key(open(signing_keys[0], "rb").read()) certs = load_pem_certs(signing_keys[1].read_bytes()) def signer(digest, digest_algo): return sign_signer_digest(priv_key, digest_algo, digest) assert sign_file(test_file, signed_exe, digest_algo, certs, signer) # Check that we have 1 certificate in the signature if test_file in TEST_PE_FILES: assert is_pefile(test_file) with signed_exe.open("rb") as f: certificates = get_certificates(f) sigs = get_signatures_from_certificates(certificates) assert len(certificates) == 1 assert len(sigs) == 1 assert len(sigs[0]["certificates"]) == 1 assert verify_pefile(f)
async def sign_file( infile, outfile, digest_algo, certs, signer, url=None, comment=None, crosscert=None, timestamp_style=None, timestamp_url=None, ): """Sign a PE or MSI file. Args: infile (str): Path to the unsigned file outfile (str): Path to where the signed file will be written digest_algo (str): Which digest algorithm to use. Generally 'sha1' or 'sha256' certs (str): Path to where the PEM encoded public certificate(s) are located signer (function): Function that takes (digest, digest_algo) and returns bytes of the signature. Normally this will be using a private key object to sign the digest. url (str): A URL to embed into the signature comment (str): A string to embed into the signature crosscert (str): Extra certificates to attach to the signature timestamp_style (str): What kind of signed timestamp to include in the signature. Can be None, 'old', or 'rfc3161'. timestamp_url (str): URL for the timestamp server to use. Required if timestamp_style is set. Returns: True on success False otherwise """ infile = Path(infile) outfile = Path(outfile) try: log.debug("Generating dummy signature") old_sig = get_dummy_signature( infile, digest_algo, url=url, comment=comment, crosscert=crosscert ) except OSError: log.error("Couldn't generate dummy signature") log.debug("Exception:", exc_info=True) return False try: log.debug("Re-signing with real keys") old_sig = get_signeddata(old_sig) if crosscert: crosscert = Path(crosscert) certs.extend(load_pem_certs(crosscert.read_bytes())) newsig = await resign(old_sig, certs, signer) except Exception: log.error("Couldn't re-sign") log.debug("Exception:", exc_info=True) return False if timestamp_style == "old": ci = der_decode(newsig, ContentInfo())[0] sig = der_decode(ci["content"], SignedData())[0] sig = await winsign.timestamp.add_old_timestamp(sig, timestamp_url) ci = ContentInfo() ci["contentType"] = id_signedData ci["content"] = sig newsig = der_encode(ci) elif timestamp_style == "rfc3161": ci = der_decode(newsig, ContentInfo())[0] sig = der_decode(ci["content"], SignedData())[0] sig = await winsign.timestamp.add_rfc3161_timestamp( sig, digest_algo, timestamp_url ) ci = ContentInfo() ci["contentType"] = id_signedData ci["content"] = sig newsig = der_encode(ci) try: log.debug("Attaching new signature") write_signature(infile, outfile, newsig) except Exception: log.error("Couldn't write new signature") log.debug("Exception:", exc_info=True) return False log.debug("Done!") return True