PyDERASN: comme j'ai ajouté le support du big data

Je continue le dernier article sur PyDERASN - le codec gratuit ASN.1 DER / CER / BER en Python. Au cours de la dernière année, depuis le moment de sa rédaction, en plus de toutes les petites choses, de petites corrections et une vérification des données encore plus rigoureuse (bien qu'avant c'était déjà le plus strict des codecs gratuits que je connaisse), une fonctionnalité pour travailler avec de grands volumes de données est apparue dans cette bibliothèque - pas rampant dans la RAM. Je veux en parler dans cet article.

Navigateur ASN.1

Problèmes / tâches


  • 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 . .


Quels objets ASN.1 dans la pratique peuvent être gourmands en ressources, prendre beaucoup d'espace mémoire? Seule SEQUENCE OF / SET OF contenant de nombreux objets (des centaines de milliers dans le cas de CACert.org), et toutes sortes de * STRING (comme dans le deuxième cas problématique). Étant donné que l'une des vertus d'un programmeur est la paresse (selon Larry Wall), nous allons essayer de surmonter les problèmes de consommation des ressources avec un changement minimal dans le code de la bibliothèque.

* Les objets STRING , du point de vue du codec, ne diffèrent presque pas les uns des autres et sont implémentés dans la bibliothèque par une classe commune. Qu'est-ce que l'encodage dans OCTET STRING dans DER?

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

Évidemment, la valeur n'a pas besoin d'être en RAM tout ce temps. Il doit s'agir d'un objet de type octets dont vous pouvez connaître la longueur.

memoryview remplit ces conditions. Et pourtant, la visualisation de la mémoire peut être effectuée par mmap sur n'importe quel fichier temporaire qui est déjà réduit de 20 Go en 10 Go pour les copies de la base de données CMS: il suffit de spécifier comme valeur des STRING OCTET (ou tout autre * STRING ) similaire à memoryview . Pour le créer, PyDERASN a une fonction d'assistance:

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

Si nous voulons décoder une énorme quantité de données, alors memoryview peut également être utilisé pour décoder:

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

Nous considérons que le problème avec * STRING est partiellement résolu. Retour à la création d'une énorme CRL. Quelle est sa structure?

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

Une longue liste de petites structures RevokedCertificate . Dans ce cas, ne contenant que le numéro de série du certificat et l'heure de sa révocation. Quel est son codage DER?

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

Juste concaténation des représentations DER de chaque élément individuel de cette liste. Avons-nous besoin d'avoir à l'avance toutes ces centaines de milliers d'objets au cours de leur simple sérialisation en 15 octets de représentation DER? Évidemment pas. Par conséquent, nous remplaçons la liste des objets par un itérateur / générateur. Très probablement, la création de la CRL sera basée sur les données du SGBD:

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

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

Maintenant, nous ne consommons presque pas de mémoire lors de la création d'une telle CRL - nous avons seulement besoin d'un endroit pour travailler avec sa représentation DER: dans ce cas, nous parlons de quelques dizaines de mégaoctets (par opposition à une liste d'un demi-gigaoctet d'objets RevokedCertificate). Mais il y a une différence de comportement: le contrôle de taille SEQUENCE OF / SET OF ne se produit qu'après épuisement de l'itérateur, et non au moment d'affecter une valeur.

Codage de streaming DER


C'est impossible. Après tout, c'est DER - les longueurs de tous les éléments TLV (tag + longueur + valeur) doivent être connues à l'avance!

Mais j'aimerais tellement! Après tout, nous avons encore besoin de 10 Gio de mémoire pour stocker la représentation DER de la copie de la base de données: raw = cms.encode () ! Idéalement, je veux transmettre un certain écrivain où la représentation sérialisée serait écrite. Peut-être transférer le descripteur de fichier, laisser des espaces réservés dans le fichier à la place des longueurs, puis les remplir en faisant rechercher? Malheureusement, la longueur (respectivement, et l'espace réservé) n'est pas connue à l'avance.

PyDERASN a reçu la possibilité d'un codage DER en deux passes. Lors de la première passe, la connaissance des longueurs des objets est collectée, créant un état temporaire. Le second est en streamingEncodage DER dans un certain écrivain , déjà possible grâce à la connaissance des longueurs. L'implémentation de ceci est apparue simple, ajoutant simplement un peu de méthodes à deux passes à chacun des types de base ASN.1. Puisque la traversée des objets est strictement déterminée (D - distinguée!), Alors, pour stocker les longueurs nécessaires, une liste simple est maintenue, à laquelle, pendant que l'arborescence entière des objets est parcourue, des valeurs de longueur sont ajoutées. Pour certains types, la longueur est fixe. Pour certains, il n'est nécessaire que pour signaler à un conteneur en amont ( SEQUENCE / SET , SEQUENCE OF / SET OF , EXPLICIT TAG ). Par exemple, l'état de longueur d'une liste de deux certificats révoqués ressemble à ceci:

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

(42, [40, 18, 18])

Dans ce cas, nous devons connaître la longueur de la valeur uniquement pour chacun des certificats révoqués et la liste entière dans son ensemble. La longueur des entiers et du temps codé (dans DER c'est une longueur fixe) dans l'état n'est pas stockée comme inutile. En conséquence, pour notre CRL CACert.org, une telle liste prend un peu plus de 3,5 Mio, et pour le CMS géant, dans lequel presque tout le poids tombe sur un seul champ avec une copie de la base de données, cela prend environ 0,5 Kio.

Le codage en deux passes est effectué par deux appels:

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

La première passe indique également la longueur totale des données, qui peuvent être utilisées pour vérifier l'espace libre ou l'allouer par un appel posix_fallocate .

En utilisant la fonction d'assistance encode2pass (obj), vous pouvez effectuer un codage à deux passes en mémoire. Pourquoi? Il peut être beaucoup plus économique en termes de consommation de mémoire, car il ne stockera pas, dans le cas de CACert.org CRL, 416k + petites lignes binaires jointes par un b "". Appel Join () . Cependant, cela nécessite plus de temps processeur, car tous les objets devront être parcourus deux fois.

Maintenant, nous pouvons encoder un CMS arbitrairement grand dans DER, pratiquement sans consommer de mémoire. Mais dans le cas de CRL, nous avons utilisé le générateur de révocation de certificats, qui s'épuisera après la fin de la première passe. Que faire? Réinitialisez-le à nouveau!

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

Bien sûr, nous sommes obligés de nous assurer que le résultat de l'itérateur sera exactement le même, sinon nous obtiendrons un DER cassé. Si les données sont extraites du curseur SGBD, n'oubliez pas le niveau d'isolement des transactions REPEATABLE READ et le tri.

Encodage en flux réel: CER


Comme vous le savez, DER (règles de codage distinctes) est un sous-ensemble de BER (règles de codage de base) qui régule strictement les règles de codage d'une et d'une seule façon. Cela lui permet d'être utilisé dans des tâches cryptographiques. Mais il existe un autre sous-ensemble remarquable de BER: CER (règles de codage canoniques). Comme DER, il n'a qu'une seule représentation possible des données. CER diffère de DER à plusieurs égards, mais ils vous permettent d'effectuer un encodage des données en streaming réel. Malheureusement, CER n'est pas devenu aussi populaire que DER.

En omettant des différences moins visibles (telles que les balises de tri dans SET), CER a deux différences fondamentales avec 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
    

Tout cela (plus quelques petites choses) permet au streaming (avec 1000 octets de tampon pour les chaînes) de coder tous les objets. De même, vous pouvez utiliser mmap et les itérateurs. L'encodage CER se fait simplement en appelant la méthode .encode_cer (writer) . Malheureusement, PyDERASN n'est pas encore en mesure de vérifier la validité du CER pendant le décodage, nous sommes donc obligés de décoder les données en tant que BER.

La norme CMS, en passant, nécessite un codage en BER (les deux DER et CER, automatique, sont BER). Par conséquent, nous pouvons coder notre énorme copie de la base de données dans CER CMS sans DER à deux passes. Cependant , SignedData doit avoir un élément SignedAttributes codé en DER, tout comme le certificat X.509.certificats. PyDERASN vous permet de forcer l'utilisation de DER dans des structures données en ajoutant simplement l' attribut der_forced = True .

Décodage de flux: mode evgen


Nous avons appris à coder, mais seul mmap aidera au décodage . Un "vrai" décodeur de flux, qui a des boutons de contrôle sous la forme de "donnez-moi plus de données", "vous êtes ici", une sorte d'état - nécessiterait une modification radicale de PyDERASN. Et, personnellement, je ne vois pas que ce serait plus pratique que la solution actuelle.

Et la solution actuelle est extrêmement simple. Dans le processus de décodage, nous avons entre nos mains divers objets primitifs décodés entre nos mains, à partir desquels sont assemblés des composants supérieurs (construits), dont d'autres composants, etc. ... Nous accumulons des objets pour les rendre composites et, atteignant le sommet donnez-nous un grand objet. Pourquoi ne pas «distribuer» immédiatement toutes sortes de décodagesobjets, dès qu'ils apparaissent sur nos mains? Autrement dit, pour renvoyer non pas un objet final, mais un générateur qui génère de nombreux objets décodés. En fait, dans PyDERASN maintenant toutes les méthodes de décodage sont devenues des générateurs de tels "événements" (génération d'événements, evgen).

Si nous activons le mode de décodage evgen de notre énorme CRL CACert.org, nous verrons l'image suivante:

$ 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

  • Au début du décodage, nous avons vu la balise CertificateList SEQUENCE , la longueur des données, mais l'objet n'est pas encore connu pour savoir s'il peut être décodé jusqu'à la fin. Jusqu'à présent, nous ne sommes en train d'y travailler.
  • SEQUENCE: version, INTEGER. . , . ( 10)
  • signature, SEQUENCE- : algorithm parameters. OBJECT IDENTIFIER ANY . ( 15, 26)
  • , , signature SEQUENCE , , : . ( 13)
  • , RevokedCertificate . ( 186, 206, ..)
  • tbsCertList . ( 5)
  • CertificateList, SEQUENCE, , , . ( 0)

Bien sûr, tous les * STRING et les listes ( * OF ) n'ont pas de véritable sens. Dans le cas de DER, connaissant .offset et .vlen, vous pouvez lire la valeur d'une ligne d'un fichier (un morceau de mémoire?). Les objets de séquence peuvent être collectés et agrégés selon vos besoins, tout en recevant tous les événements.

Chemin décodé et evgen_mode_upto


Comment comprendre quel type d'objet, quel type de nombre entier nous avons sous la main? Chaque objet a son propre chemin dit de décodage, qui identifie de manière unique un objet spécifique dans la structure. Par exemple, pour les événements de chemin de décodage CRL CACert.org:

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

Voici comment imprimer la liste des numéros de série de révocation de certificats à partir de cette liste de révocation de certificats:

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))

La structure RevokedCertificate peut contenir de nombreuses informations, y compris diverses extensions. De manière purement technique, nous obtenons toutes les données sur le certificat révoqué en mode evgen, mais l'agrégation des événements liés à un élément revokedCertificates n'est pas très pratique. Étant donné que chaque certificat révoqué final , dans la pratique, ne prendra pas beaucoup de place, ce serait bien de l'avoir tout de même, tous les objets ne sont pas si soigneusement «triés» en événements. PyDERASN permet à la liste de spécifier les chemins de décodage auxquels le mode evgen est désactivé. Nous pouvons donc définir le chemin de décodage (n'importe quel élément de la liste ("tbsCertList", "revokedCertificates") ) sur lequel nous voulons obtenir l' objet RevokedCertificate complet :

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"]))

Chaînes agrégées: agg_octet_string


Maintenant, nous n'avons aucun problème à décoder des objets de toute taille. Il n'y a pas de problème non plus à décoder un CMS encodé en DER avec une copie de la base de données: nous attendons un événement avec un chemin de décodage pointant vers les données signées / cryptées dans le CMS et traitant les données du fichier en utilisant offset + vlen. Mais que faire si le CMS était au format CER? Ensuite, offset + vlen n'aidera pas, car tous nos 10 Gio sont divisés en morceaux de 1000 octets, entre lesquels par l'en-tête DER. Mais que se passe-t-il si nous avons un BER dans lequel l'imbrication de * STRING peut être n'importe quoi?

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

Lors du décodage en mode evgen, pour chaque pièce, nous obtenons l'événement correspondant et il nous suffit de collecter uniquement des événements STRING primitifs (non construits) , où offset + vlen contiennent de vraies données. PyDERASN a une fonction d' aide pratique agg_octet_string qui effectue cela. Il lui suffit de passer un générateur d'événements, le chemin de décodage «en dessous» dont il faut agréger une chaîne, des données binaires (ou memoryview ) et un écrivain - une fonction qui est appelée avec chaque donnée reçue. Nous voulons calculer le hachage SHA512 et sauvegarder en même temps le contenu du CMS encapContentInfo.eContent ? Trouver l'emplacement du champ de contenu (dans lequel SignedData sera situé), puis décoder son contenu CER, en écrivant simultanément au FS et en hachant:

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)

Ici, un autre utilitaire write_full est utilisé , qui appelle l' écrivain jusqu'à ce que toutes les données soient écrites, car, en général, lors de l'écriture dans un fichier, le système d'exploitation n'est pas requis pour traiter toutes les données transférées et vous devez continuer le processus jusqu'à ce qu'il soit complètement écrit.

Un couple affectueux à propos de SET OF


D'un point de vue purement technique, SET OF ne peut pas être codé à la volée dans les codages DER et CER, car les représentations codées de tous les éléments doivent être triées. Ni le CER ni un DER à deux passes n'aidera ici. La norme ASN.1 moderne ne recommande donc pas l'utilisation à la fois de SET (nécessitant un tri similaire dans DER) et de SET OF .

Et quelle est cette image au début de l'article?


C'est dans PyDERASN qu'un navigateur ASN.1 interactif et sans prétention est apparu , ce qui m'a personnellement aidé plusieurs fois à écrire des gestionnaires pour des structures complexes. Vous permet de parcourir toute la structure décodée, en montrant tous les détails sur chaque objet, son emplacement dans les données binaires, le chemin de décodage. De plus, tout élément peut être enregistré dans un fichier séparé, comme les certificats ou les listes de révocation de certificats inclus dans le CMS.

Sergey Matveev , cipherpunk , développeur Python / Go, spécialiste en chef du FSUE «Centre scientifique et technique« Atlas ».

All Articles