PyDERASN:随着我添加了大数据支持

我继续上一篇有关PyDERASN的文章-Python中免费的ASN.1 DER / CER / BER编解码器。在过去的一年中,从编写之时起,除了所有微小的事情,小的更正,甚至是更加严格的数据验证(尽管在我所熟知的免费编解码器之前,它已经是最严格的),用于处理大量数据的功能已出现在此库中-潜入RAM。我想在本文中谈论这一点。

ASN.1浏览器

问题/任务


  • CRL:
    , (CA) X.509 . , CRL (certificate revocation list). CRL CACert.org, 8.72 MiB, PyDERASN- Python 3.6 ( asn1crypto pyasn1 ). 416 . . , . .
  • CMS:
    CMS (Cryptographic Message Syntax)
    // . ,
    SignedData , ,
    , - EnvelopedData
    .

    CMS. , CMS detached data, - , . 10 GiB 20 GiB : 10 GiB . .


实际上,哪些ASN.1对象会占用大量资源,占用大量内存空间?仅SEQUENCE OF / SET OF包含许多对象(在CACert.org中为成千上万个)和所有* STRING(在第二种有问题的情况下)。由于程序员的优点之一是懒惰(根据Larry Wall),所以我们将尝试通过对库代码进行最少的更改来克服资源消耗的问题。

*从编解码器的角度来看,STRING对象几乎彼此没有区别,并由一个公共类在库中实现。DER的OCTET STRING中的编码是什么

return tag + len_encode(len(value)) + value

显然,不必一直都在RAM中。它必须是类似字节的对象,从中可以找到长度。

memoryview满足这些条件。但是,可以通过mmap来完成对已在内存中减少的任何临时文件的memoryview的工作。对于CMS数据库副本,该临时文件的一半需要20 GiB 10 GiB的一半:只需将OCTET STRING(或任何其他* STRING的值)指定为类似于memoryview要创建它,PyDERASN具有帮助功能:

from pyderasn import file_mmaped
with open("dump.sql.zst", "rb") as fd:
    ... = OctetString(file_mmaped(fd))

如果我们要解码大量数据,那么memoryview也可以用于解码:

from pyderasn import file_mmaped
with open("dump.sql.zst", "rb") as fd:
    obj = Schema.decode(file_mmaped(fd))

我们认为* STRING的问题已部分解决。返回创建巨大的CRL。它的结构是什么样的?

CertificateList SEQUENCE
. tbsCertList: TBSCertList SEQUENCE
. . version: Version INTEGER v2 (01) OPTIONAL
. . signature: AlgorithmIdentifier SEQUENCE
. . issuer: Name CHOICE rdnSequence
. . thisUpdate: Time CHOICE utcTime
. . nextUpdate: Time CHOICE utcTime OPTIONAL
. . revokedCertificates: RevokedCertificates SEQUENCE OF OPTIONAL
. . . 0: RevokedCertificate SEQUENCE
. . . . userCertificate: CertificateSerialNumber INTEGER 17 (11)
. . . . revocationDate: Time CHOICE utcTime UTCTime 2003-04-01T14:25:08
. . . 1: RevokedCertificate SEQUENCE
. . . . userCertificate: CertificateSerialNumber INTEGER 20 (14)
. . . . revocationDate: Time CHOICE utcTime UTCTime 2002-10-01T02:18:01

                                 [...]

. . . 415753: RevokedCertificate SEQUENCE
. . . . userCertificate: CertificateSerialNumber INTEGER 1341859 (14:79:A3)
. . . . revocationDate: Time CHOICE utcTime UTCTime 2020-02-08T06:51:56
. . . 415754: RevokedCertificate SEQUENCE
. . . . userCertificate: CertificateSerialNumber INTEGER 1341860 (14:79:A4)
. . . . revocationDate: Time CHOICE utcTime UTCTime 2020-02-08T06:53:01
. . . 415755: RevokedCertificate SEQUENCE
. . . . userCertificate: CertificateSerialNumber INTEGER 1341861 (14:79:A5)
. . . . revocationDate: Time CHOICE utcTime UTCTime 2020-02-08T07:25:06
. signatureAlgorithm: AlgorithmIdentifier SEQUENCE
. signatureValue: BIT STRING 4096 bits

小型RevokedCertificate结构的一长串清单在这种情况下,仅包含证书的序列号及其撤销时间。它的DER编码是什么?

value = b"".join(revCert.encode() for revCert in revCerts)
return tag + len_encode(len(value)) + value

只是此列表中每个元素的DER表示的串联。在仅被序列化为DER表示形式的15个字节的过程中,我们是否需要预先拥有所有成千上万个对象?明显不是。因此,我们用迭代器/生成器替换对象列表。最有可能的是,CRL的创建将基于来自DBMS的数据:

def revCertsGenerator(...):
    for row in db.cursor:
        yield RevokedCertificate((
            ("userCertificate", CertificateSerialNumber(row.serial)),
            ("revocationDate", Time(("utcTime", UTCTime(row.revdate)))),
        ))

crl["tbsCertList"]["revokedCertificates"] = revCertsGenerator()

现在,在创建这样的CRL时我们几乎不消耗内存-我们只需要一个使用其DER表示形式的地方即可:在这种情况下,我们正在谈论的是几十兆字节(而不是一个半千兆字节的RevokedCertificate对象列表)。但是行为上存在差异:大小检查的SEQUENCE OF / SET OF仅在迭代器用尽之后才发生,而不是在分配值时发生。

DER流编码


是不可能的。毕竟,这就是DER-必须预先知道所有TLV(标签+长度+值)元素的长度!

但是我非常想要!毕竟,我们仍然需要10 GiB的内存来存储数据库副本的DER表示形式:raw = cms.encode()!理想情况下,我想传达一位特定的作者,在其中编写序列化表示。也许传输文件描述符,将占位符留在文件中的长度位置,然后通过查找来填充它们?不幸的是,长度长度(分别和占位符)也不是事先知道的。

PyDERASN已经接受了两次通过DER编码的可能性。在第一遍中,收集有关对象长度的知识,从而创建一个临时状态。第二个是流媒体DER编码成一定的作家,已经有可能由于长度的知识。实现起来很简单,只需向每个ASN.1基本类型添加一点两次通过方法。由于对象的遍历是严格确定的(D-区分!),因此,为了存储必要的长度,将维护一个简单的列表,随着遍历对象的整个树,添加一个长度值。对于某些类型,长度是固定的。对于某些文件,仅需要向上游容器报告(SEQUENCE / SETSEQUENCE OF / SET OFEXPLICIT TAG)。例如,两个已撤销证书的列表的长度状态如下所示:

revCert = RevokedCertificate((
    ("userCertificate", CertificateSerialNumber(123)),
    ("revocationDate", Time(("utcTime", UTCTime(datetime.utcnow())))),
))
revs = RevokedCertificates([revCert, revCert])

(42, [40, 18, 18])

在这种情况下,我们仅需要了解每个RevokedCertificate和整个列表的值长度。状态中的整数长度和编码时间(在DER中为固定长度)不会被不必要地存储。结果,对于我们的CACert.org CRL,这样的列表占用的内存比3.5 MiB略多一点;对于巨型CMS,其中几乎所有的权重都落在一个具有数据库副本的单个字段上,大约需要0.5 KiB。

通过两次调用执行两次通过编码:

fulllen, state = obj.encode1st()
with open("result", "wb") as fd:
    obj.encode2nd(fd.write, iter(state))

第一遍还报告数据的完整长度,可用于检查可用空间或通过某些posix_fallocate调用分配可用空间

使用辅助函数encode2pass(obj),可以对内存执行两次遍历编码。做什么的?它可以显着降低内存消耗的经济性,因为对于CACert.org CRL,它不会存储由b“”。Join()调用连接的416k +小二进制行。但是,这需要更多的处理器时间,因为所有对象都必须走两次。

现在,我们可以在DER中编码任意大的CMS,几乎不需要消耗内存。但是对于CRL,我们使用了证书吊销生成器,该证书吊销生成器将在第一遍结束后用完。该怎么办?只需重新初始化即可!

_, state = crl.encode1st()
crl["tbsCertList"]["revokedCertificates"] = revCertsGenerator()
crl.encode2nd(writer, iter(state))

当然,我们有义务确保迭代器的结果将完全相同,否则我们将获得损坏的DER。如果数据是从DBMS游标中获取的,那么不要忘记REPEATABLE READ事务隔离级别和排序。

实时流编码:CER


如您所知,DER(杰出的编码规则)是BER(基本编码规则)的子集,它以一种且仅一种方式严格地调节编码规则。这使得它可以用于加密任务。但是,还有另一个值得注意的BER子集:CER(规范编码规则)。像DER一样,它只有一种可能的数据表示形式。 CER在某些细节上与DER不同,但是它们使您可以执行真正的数据流编码。不幸的是,CER没有像DER那样受欢迎。

忽略了不太明显的差异(例如SET中的排序标签),CER与DER有两个基本差异:

  • (constructed, ) indefinite (LENINDEF PyDERASN). , SEQUENCE/SET, SEQUENCE OF/SET OF, EXPLICIT TAG- :

    TAG_CONSTRUCTED || LEN(VALUE) || VALUE
    

    LENINDEF (0x80) EOC ( , ) :

    TAG_CONSTRUCTED || 80 || VALUE || 00 00
    

  • *STRING-, 1000-, chunk- 1000-. , , DER. , 512 DER:

    TAG_PRIMITIVE || LEN(512) || 512B
    

    2048 :

    TAG_CONSTRUCTED || 80 ||
        TAG_PRIMITIVE || LEN(1000) || 1000B ||
        TAG_PRIMITIVE || LEN(1000) || 1000B ||
        TAG_PRIMITIVE || LEN(48) || 48B || 00 00
    

所有这些(加上一些小东西)允许流(字符串缓冲区有1000字节)来编码任何对象。同样,您可以使用mmap和迭代器。只需调用.encode_cer(writer)方法即可完成CER编码。不幸的是,PyDERASN尚无法在解码期间验证CER的有效性,因此我们被迫将数据解码为BER。

顺便说一下,CMS标准要求使用BER(DER和CER都是自动的,都是BER)编码。因此,我们可以将庞大的数据库副本编码为CER CMS,而无需进行两次通过的DER。但是要求SignedData具有以DER编码SignedAttributes元素,如X.509 证书一样。证书。PyDERASN允许您通过简单地添加der_forced = True属性来强制在给定结构中使用DER

流解码:evgen模式


我们学会了编码,但是只有mmap可以帮助解码。一个“真实”流解码器,具有“给我更多数据”,“您在这里”等某种状态的控制旋钮,将需要对PyDERASN进行彻底更改。而且,就我个人而言,我认为这不会比当前解决方案更方便。

当前的解决方案非常简单。在解码的过程中,我们手中有各种已解码的原始对象,它们是由它们组装而成的高级组件(已构造的),其中包括其他组件,等等。...我们累积对象以使它们组合并到达最顶端给我们一个大对象。为什么不立即“放弃”各种解码对象,只要它们出现在我们手上?也就是说,不是返回最终对象,而是返回生成许多解码对象的生成器。实际上,在PyDERASN中,现在所有的解码方法都已成为此类“事件”的生成器(事件生成,evgen)。

如果启用巨大的CACert.org CRL的evgen解码模式,我们将看到以下图片:

$ python -m pyderasn --schema tests.test_crl:CertificateList --evgen revoke.crl
[][T,L,  V len]
     10   [1,1,      1]   . . version: Version INTEGER v2 (01) OPTIONAL
     15   [1,1,      9]   . . . algorithm: OBJECT IDENTIFIER 1.2.840.113549.1.1.13
     26   [0,0,      2]   . . . parameters: [UNIV 5] ANY OPTIONAL
     13   [1,1,     13]   . . signature: AlgorithmIdentifier SEQUENCE
     34   [1,1,      3]   . . . . . . type: AttributeType OBJECT IDENTIFIER 2.5.4.10
     39   [0,0,      9]   . . . . . . value: [UNIV 19] AttributeValue ANY
     32   [1,1,     14]   . . . . . 0: AttributeTypeAndValue SEQUENCE
     30   [1,1,     16]   . . . . 0: RelativeDistinguishedName SET OF

                                 [...]

    188   [1,1,      1]   . . . . userCertificate: CertificateSerialNumber INTEGER 17 (11)
    191   [1,1,     13]   . . . . . utcTime: UTCTime UTCTime 2003-04-01T14:25:08
    191   [0,0,     15]   . . . . revocationDate: Time CHOICE utcTime
    191   [1,1,     13]   . . . . . utcTime: UTCTime UTCTime 2003-04-01T14:25:08
    186   [1,1,     18]   . . . 0: RevokedCertificate SEQUENCE
    208   [1,1,      1]   . . . . userCertificate: CertificateSerialNumber INTEGER 20 (14)
    211   [1,1,     13]   . . . . . utcTime: UTCTime UTCTime 2002-10-01T02:18:01
    211   [0,0,     15]   . . . . revocationDate: Time CHOICE utcTime
    206   [1,1,     18]   . . . 1: RevokedCertificate SEQUENCE

                                 [...]

9144992   [0,0,     15]   . . . . revocationDate: Time CHOICE utcTime
9144985   [1,1,     20]   . . . 415755: RevokedCertificate SEQUENCE
    181   [1,4,9144821]   . . revokedCertificates: RevokedCertificates SEQUENCE OF OPTIONAL
      5   [1,4,9144997]   . tbsCertList: TBSCertList SEQUENCE
9145009   [1,1,      9]   . . algorithm: OBJECT IDENTIFIER 1.2.840.113549.1.1.13
9145020   [0,0,      2]   . . parameters: [UNIV 5] ANY OPTIONAL
9145007   [1,1,     13]   . signatureAlgorithm: AlgorithmIdentifier SEQUENCE
9145022   [1,3,    513]   . signatureValue: BIT STRING 4096 bits
      0   [1,4,9145534]  CertificateList SEQUENCE

  • 在解码开始时,我们看到了CertificateList SEQUENCE标签,即数据长度,但是尚不清楚对象是否可以解码到结尾。到目前为止,我们只是在努力中。
  • SEQUENCE: version, INTEGER. . , . ( 10)
  • signature, SEQUENCE- : algorithm parameters. OBJECT IDENTIFIER ANY . ( 15, 26)
  • , , signature SEQUENCE , , : . ( 13)
  • , RevokedCertificate . ( 186, 206, ..)
  • tbsCertList . ( 5)
  • CertificateList, SEQUENCE, , , . ( 0)

当然,所有* STRING和列表(* OF)都不具有实际含义。对于DER,如果知道.offset.vlen,则可以从文件(一块内存?)中读取一行的值。可以在接收所有事件的同时根据需要收集和汇总序列对象。

解码路径和evgen_mode_upto


如何理解手头上的什么样的对象,什么样的整数每个对象都有其自己的所谓解码路径,该路径唯一地标识结构中的特定对象。例如,对于CACert.org CRL解码路径事件:

tbsCertList:version
tbsCertList:signature:algorithm
tbsCertList:signature:parameters
tbsCertList:signature
tbsCertList:issuer:rdnSequence:0:0:type
                                 [...]
tbsCertList:issuer:rdnSequence
tbsCertList:issuer
                                 [...]
tbsCertList:revokedCertificates:0:userCertificate
tbsCertList:revokedCertificates:0:revocationDate:utcTime
tbsCertList:revokedCertificates:0:revocationDate
tbsCertList:revokedCertificates:0
tbsCertList:revokedCertificates:1:userCertificate
tbsCertList:revokedCertificates:1:revocationDate:utcTime
tbsCertList:revokedCertificates:1:revocationDate
tbsCertList:revokedCertificates:1
                                 [...]
tbsCertList:revokedCertificates:415755:userCertificate
tbsCertList:revokedCertificates:415755:revocationDate:utcTime
tbsCertList:revokedCertificates:415755:revocationDate
tbsCertList:revokedCertificates:415755
tbsCertList:revokedCertificates
tbsCertList
signatureAlgorithm:algorithm
signatureAlgorithm:parameters
signatureAlgorithm
signatureValue

这是我们可以从此CRL打印证书吊销序列号列表的方式:

raw = file_mmaped(open("....crl", "rb"))
for decode_path, obj, tail in CertificateList().decode_evgen(raw):
    if (len(decode_path) == 5) and (decode_path[-1] == "userCertificate"):
        print(int(obj))

RevokedCertificate 结构可以包含很多信息,包括各种扩展名。纯粹从技术上讲,我们在evgen模式下获取有关已吊销证书的所有数据,但是汇总与一个吊销证书元素相关的事件不是很方便。由于实际上每个最终的RevokedCertificate都不会占用太多空间,所以拥有相同的状态将是一件很棒的事情,并非所有对象都被如此彻底地“分类”为事件。PyDERASN允许列表指定禁用evgen模式的解码路径。因此,我们可以设置要获取完整RevokedCertificate对象的解码路径((“(tbsCertList”,“ revokedCertificates”)列表中的任何元素

for decode_path, obj, _ in CertificateList().decode_evgen(raw, ctx={
    "evgen_mode_upto": (
        (("tbsCertList", "revokedCertificates", any), True),
    ),
}):
    if (len(decode_path) == 3) and (decode_path[1] == "revokedCertificates"):
        print(int(obj["userCertificate"]))

汇总字符串:agg_octet_string


现在,我们可以轻松解码任何大小的对象。使用数据库副本对DER编码的CMS进行解码也没有问题:我们正在等待事件,该事件的解码路径指向CMS中的已签名/加密数据,并使用offset + vlen处理文件中的数据。但是,如果CMS为CER格式怎么办?然后,偏移量+ vlen将无济于事,因为我们所有的10个GiB都被分为1000字节,在这两个字节之间是DER标头。但是,如果我们有一个BER,其中* STRING的嵌套可以是任何东西呢?

SOME STRING[CONSTRUCTED]
    OCTET STRING[CONSTRUCTED]
        OCTET STRING[PRIMITIVE]
            DATA CHUNK
        OCTET STRING[PRIMITIVE]
            DATA CHUNK
        OCTET STRING[PRIMITIVE]
            DATA CHUNK
    OCTET STRING[PRIMITIVE]
        DATA CHUNK
    OCTET STRING[CONSTRUCTED]
        OCTET STRING[PRIMITIVE]
            DATA CHUNK
        OCTET STRING[PRIMITIVE]
            DATA CHUNK
    OCTET STRING[CONSTRUCTED]
        OCTET STRING[CONSTRUCTED]
            OCTET STRING[PRIMITIVE]
                DATA CHUNK

在evgen模式下解码时,对于每个片段,我们都会获得相应的事件,并且仅收集原始(未构造)* STRING事件就足够了,其中offset + vlen包含真实数据。 PyDERASN具有方便的辅助函数agg_octet_string来执行此操作。她通过一个事件生成器就足够了,该事件生成器的“解码路径”在其“下方”必须聚合一个字符串,二进制数据(或memoryview)和writer-每个接收到的数据都调用一个函数。我们要计算SHA512哈希值,同时保存encapContentInfo.eContent CMS 的内容?找到内容字段的位置SignedData将位于其中)),然后解码其CER内容,同时写入FS和哈希:

fdIn = open("data.p7m", "rb")
raw = file_mmaped(fdIn)
for decode_path, obj, _ in ContentInfo().decode_evgen(raw, ctx={"bered": True}):
    if decode_path == ("content",):
        content = obj
        break
hasher_state = sha512()
fdOut = open("dump.sql.zst", "wb")
def hash_n_save(data):
    write_full(fdOut, data)
    hasher_state.update(data)
    return len(data)
evgens = SignedData().decode_evgen(
    raw[content.offset:],
    offset=content.offset,
    ctx={"bered": True},
)
agg_octet_string(evgens, ("encapContentInfo", "eContent"), raw, hash_n_save)

在这里,使用了另一个write_full实用程序,它会调用writer直到写入所有数据为止,因为通常在写入文件时,不需要OS处理所有传输的数据,并且您需要继续执行该过程直到将其完全写入为止。

关于SET OF的一些深情


纯粹从技术上讲,SET OF不能使用DER和CER编码即时编码,因为必须对所有元素的编码表示形式进行排序。CER或两次通过的DER都不会对这里有所帮助。因此,现代ASN.1标准不建议同时使用SET(要求在DER中进行类似的排序)和SET OF

文章开头的图片是什么?


正是在PyDERASN中,出现了一个互动的,朴实的ASN.1浏览器,它亲自帮助我多次编写了复杂结构的处理程序。使您可以遍历整个解码的结构,显示有关每个对象的完整详细信息,其在二进制数据中的位置以及解码路径。此外,任何项目都可以保存在单独的文件中,例如CMS中包含的证书或CRL。

Sergey Matveevcipherpunk,Python / Go-developer,FSUE“科学技术中心” Atlas的首席专家。

All Articles