跳转到主要内容

用于验证加载Python模块的signed imports

项目描述

此模块通过使用加密签名散列来验证在加载之前Python模块的导入钩子。它与PEP 302兼容,旨在补充主机操作系统(例如Microsoft Authenticode,Apple OSX Code Signing)的代码签名功能,该功能可能能够验证Python可执行文件本身,但不能验证在运行时动态加载的代码。

它主要用于冻结的Python应用程序,或其他代码不期望更改的情况。在标准的Python解释器中几乎毫无用处。

如果您只是需要一个黑盒解决方案,可以尝试以下函数调用,使用新随机生成的密钥对您的应用程序进行签名

signedimp.tools.sign_py2exe_app(path_to_app_dir)
signedimp.tools.sign_py2app_bundle(path_to_app_dir)
signedimp.tools.sign_cxfreeze_app(path_to_app_dir)

这些函数修改了冻结的Python应用程序,使其在加载模块之前验证其完整性,使用仅为该应用程序生成的一次性密钥。

但真正重要的是,您应该继续阅读以了解具体发生了什么。有很多需要注意的地方。

启用Signed Imports

要启用signed imports,您需要创建一个带有适当加密密钥的SignedImportManager,并将其安装到导入机制中

from signedimp import SignedImportManager, RSAKey

key = RSAKey(modulus,pub_exponent)
mgr = SignedImportManager([key])
mgr.install()

从现在起,所有导入模块的请求在允许继续之前都将与签名清单文件进行校验。如果无法验证模块,则导入将失败。

验证是在与现有导入机制协同下进行的,使用可选的loader方法get_data()。它至少与默认导入机制和zipimport模块兼容;如果你有自定义导入钩子,这些钩子没有提供此方法,或者不遵守Python导入的标准文件布局,那么它们将无法与signedimp一起使用。

目前,signedimp使用RSA密钥进行数字签名,并使用“概率签名方案”填充机制。要生成新密钥,你需要安装PyCrypto,并执行以下操作:

from signedimp.crypto.rsa import RSAKey
key = RSAKey.generate()
pubkey = key.get_public_key()

请将此密钥存储在安全的地方,你需要它来签名文件。最简单的方法是使用“save_to_file”方法。

with open("mykeyfile","wb") as f:
    key.save_to_file(f,"mypassword")

要检索密钥,例如在你的构建脚本中,可以这样做:

with open("mykeyfile","rb") as f:
    key = RSAKey.load_from_file(f,getpass())

你还必须在你的最终可执行文件中嵌入公钥,以便在验证导入时使用。signedimp.tools中的函数会为你完成这项工作——如果你正在编写自己的方案,你可以将其序列化,或者在源代码中嵌入其repr()。

清单

要验证导入,sys.path中的每个条目都必须包含一个清单文件,该文件包含每个模块的加密哈希,并由一个或多个私有密钥签名。该文件名为“signedimp-manifest.txt”,并且将使用get_data()方法从每个导入加载器请求,实际上这意味着该文件必须存在于每个目录的根目录以及sys.path中列出的每个zip文件。

清单是一个简单的文本文件。它以零个或多个行开始,这些行给出密钥指纹,然后是使用该密钥的签名;这些与哈希数据由空行分开。然后包含哈希类型标识符和每个模块哈希的一行。以下是一个简短的示例:

----
key1fingerprint b64-encoded-signature1
key2fingerprint b64-encoded-signature2

md5
76f3f13442c26fd4f1c709c7b03c6b76 os.pyc
f56dbc5ee6774e857a7ef07accdbd19b hashlib.pyc
43b74fc5d2acb6b4e417f4feff06dd81 some/data/file.txt
----

指纹和签名的格式取决于所使用的密钥类型,应将其视为ASCII块。

要创建清单文件,你需要包含私有密钥数据的密钥对象。然后你可以使用“tools”子模块中的函数。

key = RSAKey(modulus,pub_exponent,priv_exponent)

signedimp.tools.sign_directory("some/dir/on/sys/path",key)
signedimp.tools.sign_zipfile("some/zipfile/on/sys/path.zip",key)

引导

显然,在使用此模块时存在严重的引导问题——虽然我们可以验证导入,但是当我们加载此模块时,我们如何验证此模块本身的导入?为了有任何用处,它必须作为签名可执行文件的一部分来集成。有几种选择:

  • 通过篡改PyImport_FrozenModules指针,将signedimp包含在Python解释器自身的“冻结”模块中。

  • 将signedimp包含在附加到可执行文件的zip文件中,并将可执行文件本身作为sys.path上的第一个项目。

  • 使用signedimp.tools.get_bootstrap_code()函数获取可以在启动脚本中直接包含的代码,并将启动脚本嵌入到可执行文件中。

由于引导代码无法执行任何导入,因此默认情况下,所有内容(甚至加密原语!)都是用纯Python实现的。因此,速度相当慢。如果你能够安全地将hashlib或PyCrypto捆绑到可执行文件中,那么在安装签名导入管理器之前,请先导入它们,以便它知道它们是安全的。

当然,导入管理器安装后要做的第一件事就是尝试导入这些模块,以加快其余过程的导入速度。

注意事项——大多数冻结程序(例如py2exe或bbfreeze)在运行用户提供的脚本之前会运行自己的启动脚本,并且这些启动脚本通常会导入常见模块,例如“os”。你可能需要篡改冻结的exe,以便首先运行signedimp引导代码,或者将它们安全地捆绑到可执行文件中。

到目前为止,我已经为签名的py2exe、py2app和cxfreeze应用程序找到了必要的咒语,并在“signedimp.tools”中有帮助函数可以为你完成这项工作。

我不相信可以在不修改bbfreeze本身的情况下签署bbfreeze应用程序。由于bbfreeze始终将sys.path设置为library.zip和应用程序目录,因此无法将引导代码捆绑到可执行文件本身中。

注意事项

所有常规的加密注意事项都适用于此处。我不是安全专家。系统的安全性取决于你的私钥、主Python可执行文件的签名以及它运行的操作系统。此外,还有一些基于其工作方式的特定注意事项。

此模块通过包装现有的导入机制来运行。要检查模块的哈希值,它要求适当的加载器对象提供该模块的代码,验证哈希值,然后允许加载器导入它。加载器在加载模块时很可能重新从磁盘读取数据,因此在短时间内它可能会被恶意代码替换。我看不到避免这种情况的任何方法,除非替换所有现有的导入机制,但我不会这样做。

如上所述,如果你从不受信任的来源加载此模块,则该模块将毫无用处。你需要签署你的实际可执行文件,并且你需要以某种方式将一些签名的signedimp引导代码捆绑到其中。有关更多详细信息,请参阅“引导”部分。

你还需要小心不要在你安装了签名的导入管理器之前导入任何内容。(一个例外是“sys”模块,它应该始终内置到可执行文件中,因此可以安全导入。)

最后,你可能已经注意到我正在违背所有合理的加密建议,并从基本的原语(如RSA和SHA1)中自己构建方案。然而,最好依赖于像keyczar这样的第三方加密库。

  • 我希望验证代码可以作为纯Python运行,而不需要任何第三方导入,以尽可能简化引导过程。

  • 我直接从PKCS#1复制了签名方案,它与keyczar等使用的方案大致相同。这是一个非常简单且易于理解的签名协议。

  • 签名代码应在离线、受控的环境中运行,有受控的输入,因此例如计时攻击的风险很小。

  • 验证代码不能泄露任何有关私钥的信息,因为它根本不拥有任何私钥,因此它可以尽可能慢、粗糙和笨拙。

当然,我对这些点的任何方面都开放协商和专家建议。

你已经收到警告。

支持者