diff --git a/.vscode/settings.json b/.vscode/settings.json index a2fe6a7a..613c0047 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,10 +1,10 @@ { "asciidoc.extensions.enableKroki": true, "asciidoc.preview.asciidoctorAttributes": { - "imagesdir":"../images", - "source-highlighter":"highlightjs", - "stem":"latexmath" + "imagesdir": "../images", + "source-highlighter": "highlightjs", + "stem": "latexmath" }, "cSpell.language": "en,fr,fr-FR", - "asciidoc.antora.enableAntoraSupport": false + "asciidoc.antora.showEnableAntoraPrompt": true } \ No newline at end of file diff --git a/_data/authors.yml b/_data/authors.yml index 2801dc59..94f2e8b5 100644 --- a/_data/authors.yml +++ b/_data/authors.yml @@ -193,6 +193,17 @@ christophesejourne: socials: linkedin: "christophesejourne" + +quentinretourne: + name: "Quentin Retourné" + bio: "Quentin Retourné a rejoint SCIAM en mars 2018 avec une envie : utiliser la Data Science pour résoudre des problèmes complexes le plus simplement possible. Il a suivi des études de Data Science et Génie Industriel à CentraleSupélec et Columbia University à New York. Quentin a grandi à l'étranger pendant 11 ans, ce qui l'a baigné dans la diversité et lui a donné une grande faculté d'adaptation. Féru de sport, il a pratiqué la natation en compétition, et s'adonne maintenant à l'escalade et au volleyball. C'est aussi un grand fan d'énigmes !" + + job: "Consultant Sénior Data/IA" + pagesciam: "https://www.sciam.fr/equipe/quentin-retourne" + picture: quentinretourne.jpg + socials: + linkedin: "quentinretourne" + pierrelepagnol: name: "Pierre Lepagnol" bio: "PhD Student in CS @LISN/Paris-Saclay University & Data Scientist" diff --git a/_posts/2024-12-15-dechiffrement-email-python.adoc b/_posts/2024-12-15-dechiffrement-email-python.adoc new file mode 100644 index 00000000..65b865c1 --- /dev/null +++ b/_posts/2024-12-15-dechiffrement-email-python.adoc @@ -0,0 +1,362 @@ += Déchiffrement d'Emails avec Python +:showtitle: +:page-navtitle: Déchiffrement d'Emails avec Python +:page-excerpt: Découvrez comment déchiffrer des emails chiffrés avec Python en utilisant `openssl`, `asn1crypto` et `cryptography`. +:layout: post +:author: quentinretourne +:page-tags: [Tutoriel, Python, Cryptographie] +:page-vignette: email-decryption.png +:page-categories: software + +Le déchiffrement d'emails chiffrés est une tâche essentielle pour garantir la confidentialité des +communications. Dans cet article, nous présentons différentes solutions pour déchiffrer des emails +chiffrés en utilisant Python, en particulier avec l'exécutable `openssl` et les bibliothèques +`asn1crypto` et `cryptography`. Nous terminons en présentant une nouvelle méthode de déchiffrement +de mails chiffrés, développée par nos soins, qui vient d'être intégrée à `cryptography` en novembre +2024. + +== Quelques prérequis + +Avant de se plonger dans le déchiffrement des emails, il est important de comprendre quelques concepts +de base en cryptographie. + +=== La cryptographie et le chiffrement + +La cryptographie est l'art de sécuriser les communications en les transformant afin qu'elles ne +puissent être lues que par les destinataires prévus. Un message chiffré n'aura pas de sens pour un +observateur externe. En cryptographie, il s'agit donc de chiffrer et de déchiffrer des messages. +Pour ce faire, plusieurs algorithmes de chiffrement sont utilisés ; nous allons parcourir ceux qui +nous seront utiles dans cet article. + +Le https://fr.wikipedia.org/wiki/Chiffrement_RSA[chiffrement RSA] est un algorithme de cryptographie +asymétrique qui utilise une paire de clés : une clé publique pour le chiffrement et une clé privée +pour le déchiffrement. RSA est largement utilisé pour sécuriser les communications sur Internet, +notamment pour les certificats SSL/TLS et les connexions SSH. + +Par opposition au chiffrement asymétrique (dont RSA fait partie), on peut aussi chiffrer des +messages de manière symétrique. Dans ce cas, on utilise la même clé pour chiffrer et déchiffrer les +messages. Cela est utile pour d'autres cas d'usages : le chiffrement symétrique est généralement +plus rapide que le chiffrement asymétrique (même si cela dépend des algorithmes utilisés). + +Ces deux types de chiffrement sont utilisés dans le format PKCS #7 pour chiffrer des emails, que +nous allons étudier ci-dessous. + +=== Le format PKCS #7 + +Les spécifications https://en.wikipedia.org/wiki/PKCS_7[PKCS #7] (Public Key Cryptography Standards +#7) définissent un format standard pour sécuriser des messages, et notamment des emails. PKCS #7 est +un format complet, qui permet notamment de signer / vérifier des messages ainsi que les chiffrer / +déchiffrer. Dans cet article, nous allons nous concentrer sur les spécifications de "chiffrement / +déchiffrement" de PKCS #7 par opposition à la partie "signature". On les désigne aussi par les +termes "encapsulation" ou "enveloped data" en anglais. + +Dans le cas de chiffrement d'un message PKCS #7, deux chiffrements ont lieu: + +1. Le chiffrement du contenu du message, selon un algorithme de chiffrement symétrique nécessitant +une clé. Cette clé est générée de manière aléatoire. +2. Le(s) chiffrement(s) de la clé ci-dessus, cette fois-ci de façon asymétrique, pour chaque destinataire +du message. Cela est fait avec la/les clé(s) RSA publique(s) du ou des destinataires, qui doivent +être mises à disposition de l'expéditeur du message. + +Ainsi, lors du déchiffrement, chaque destinataire va : + +- Inspecter chaque clé chiffrée dans le message sous format PKCS #7, et trouver celle qui +correspond à sa clé RSA publique. +- Déchiffrer la clé symétrique avec sa clé RSA privée. +- Déchiffrer le contenu du message avec la clé symétrique fraîchement déchiffrée. + +L'avantage de cette méthode permet d'économiser du temps de calcul et de réduire la taille du +message à transporter. En effet, le contenu (qui n'a pas de limite de taille) est chiffré une seule +fois avec un algorithme symétrique, beaucoup plus rapide que ses homologues asymétriques. De plus, +c'est uniquement la clé symétrique (entre 128 et 256 bits selon les algorithmes) qui est chiffrée +plusieurs fois pour chaque destinataire par un algorithme asymétrique et stockée dans la structure +du message. + +== L'analyse d'un message chiffré + +La structure https://fr.wikipedia.org/wiki/Abstract_Syntax_Notation_One[ASN.1] (Abstract Syntax +Notation One) est un standard de notation utilisé pour représenter des données, couramment utilisé +dans les protocoles de communication et les certificats numériques. + +Nous allons étudier un message chiffré, stocké dans le fichier `enveloped.der`. Les messages PKCS#7 +sont toujours encodés dans une structure ASN.1. Ici, elle est au format DER, un format binaire +classique pour ce type de structure. + +=== Avec OpenSSL + +OpenSSL est une bibliothèque logicielle open-source qui fournit des implémentations de nombreux +protocoles de sécurité. Elle est fournie notamment sous forme d'un exécutable en ligne de commande +qui permet de manipuler des certificats, des clés et des messages chiffrés. Cet exécutable est +généralement installé par défaut dans les environnements Conda pour Python, ce qui est très +pratique. Ainsi, on peut l'utiliser tel quel via la bibliothèque built-in `subprocess` : + +[source, python] +---- +import subprocess + +instructions = [ + "openssl", + "asn1parse", <1> + "-in", + "vectors/enveloped.der", + "-inform", + "der", +] +output = subprocess.run(instructions, check=True, capture_output=True) <2> +print(output.stdout.decode()) +---- +<1> La commande `asn1parse` d' `openssl` permet d'analyser la structure ASN.1 du message chiffré. +<2> On utilise `check=True` pour lever une exception si la commande `openssl` échoue. + +Résultat : +[source, cmd] +---- +0:d=0 hl=4 l= 667 cons: SEQUENCE +4:d=1 hl=2 l= 9 prim: OBJECT :pkcs7-envelopedData <1> +15:d=1 hl=4 l= 652 cons: cont [ 0 ] +19:d=2 hl=4 l= 648 cons: SEQUENCE +23:d=3 hl=2 l= 1 prim: INTEGER :00 +26:d=3 hl=4 l= 579 cons: SET +30:d=4 hl=4 l= 575 cons: SEQUENCE +34:d=5 hl=2 l= 1 prim: INTEGER :00 +37:d=5 hl=2 l= 39 cons: SEQUENCE +39:d=6 hl=2 l= 26 cons: SEQUENCE +41:d=7 hl=2 l= 24 cons: SET +43:d=8 hl=2 l= 22 cons: SEQUENCE +45:d=9 hl=2 l= 3 prim: OBJECT :commonName <2> +50:d=9 hl=2 l= 15 prim: UTF8STRING :cryptography CA +67:d=6 hl=2 l= 9 prim: INTEGER :E712D3A0A56ED6C9 +78:d=5 hl=2 l= 13 cons: SEQUENCE +80:d=6 hl=2 l= 9 prim: OBJECT :rsaEncryption <3> +91:d=6 hl=2 l= 0 prim: NULL +93:d=5 hl=4 l= 512 prim: OCTET STRING [HEX DUMP]:08086584F1DD436CDA1FB527B243FA02 +609:d=3 hl=2 l= 60 cons: SEQUENCE +611:d=4 hl=2 l= 9 prim: OBJECT :pkcs7-data +622:d=4 hl=2 l= 29 cons: SEQUENCE +624:d=5 hl=2 l= 9 prim: OBJECT :aes-128-cbc <4> +635:d=5 hl=2 l= 16 prim: OCTET STRING [HEX DUMP]:2CD7875912507DFC7E65EA7CB86C73BB +653:d=4 hl=2 l= 16 prim: cont [ 0 ] +---- +<1> Le type de message est `pkcs7-envelopedData`. +<2> On voit bien un seul certificat, avec un numéro de série `E712D3A0A56ED6C9`. +<3> L'algorithme de chiffrement est `rsaEncryption`, avec la clé chiffrée ci-dessous. +<4> Le contenu est chiffré avec l'algorithme AES, avec une clé de 128 +bits et le mode CBC. Son contenu est ci-dessous. + +=== Avec `asn1crypto` + +On peut faire le même exercice en utilisant la librairie `asn1crypto`. Cette bibliothèque permet de +manipuler des structures ASN.1 de manière plus aisée que `openssl`, en offrant des classes Python +qui peuvent être sérialisées et désérialisées facilement en dictionnaires. Dans notre cas, prenons +l'exemple de la lecture d'une partie de la structure du message chiffré : + +[source, python] +---- +from asn1crypto import cms + +with open("vectors/enveloped.der", "rb") as file: + enveloped = file.read() + +# Charger la structure ASN.1 +content_info = cms.ContentInfo.load(enveloped) <1> +content_type: cms.ContentType = content_info["content_type"] +enveloped_data: cms.EnvelopedData = content_info["content"] + +# Le champ "Encrypted Content Info" contient le contenu chiffré +encrypted_content_info: cms.EncryptedContentInfo = enveloped_data["encrypted_content_info"] +dict(encrypted_content_info.native) +---- +<1> Il faut connaître en amont la structure ASN.1 du message pour pouvoir la charger et utiliser le +typage correctement. Cela demande une connaissance préalable des spécifications PKCS #7. + +Résultat : + +[source, python] +---- +{ + "content_encryption_algorithm": { <1> + "algorithm": "aes128_cbc", + "parameters": b",\xd7\x87Y\x12P}\xfc~e\xea|\xb8ls\xbb", + }, + "content_type": "data", + "encrypted_content": b"[tN\xcb\xdd]\x0b\xa2\xa2\x98T\xf8[t_`", +} +---- +<1> On retrouve la même structure que lors de l'analyse avec `openssl`. + +== Le déchiffrement + +Nous allons maintenant déchiffrer le message chiffré. Pour cela, nous avons besoin des différentes +informations du destinataire, notamment son certificat X.509 et sa clé privée RSA. Ensuite, nous +verrons 3 méthodes pour déchiffrer le message : avec `openssl`, avec `asn1crypto` et avec +`cryptography`. + +=== La lecture du certificat et de la clé privée + +Nous lisons le certificat X.509 et la clé privée RSA, avec la librairie `cryptography` : + +[source, python] +---- +from cryptography.hazmat.primitives.serialization import load_pem_private_key +from cryptography.x509 import load_pem_x509_certificate + +# Clé publique : certificat RSA +with open("vectors/rsa_ca.pem", "rb") as file: + certificate = load_pem_x509_certificate(file.read()) + +# Clé privée : RSA +with open("vectors/rsa_key.pem", "rb") as file: + private_key = load_pem_private_key(file.read(), password=None) +---- + +=== Avec OpenSSL +Nous utilisons `openssl` pour déchiffrer le message : + +[source, python] +---- +import subprocess + +instructions = [ + "openssl", + "smime", + "-decrypt", <1> + "-in", + "vectors/enveloped.der", + "-inkey", + "vectors/rsa_key.pem", <2> + "-inform", + "der", +] +output = subprocess.run(instructions, capture_output=True) +output.stdout.decode() +---- +<1> La commande `smime -decrypt` permet de déchiffrer un message PKCS #7. Il en existe d'autres +comme `smime -encrypt` ou `cms -decrypt` pour des spécifications PKCS # 7 plus récentes. +<2> Dans cette commande, on ne précise pas le certificat X.509. Il semble qu' `openssl` tente de +déchiffrer le message avec la clé privée fournie, sans vérifier si elle correspond au certificat +dans le message chiffré. + +=== Avec `asn1crypto` +Nous utilisons `asn1crypto` pour analyser et déchiffrer le message : + +[source, python] +---- +from asn1crypto import cms +from cryptography.hazmat.primitives import padding +from cryptography.hazmat.primitives.asymmetric import padding as asymmetric_padding +from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes + +# Charger le message chiffré +with open("vectors/enveloped.der", "rb") as file: + enveloped = file.read() + +# Charger la structure ASN.1 +content_info = cms.ContentInfo.load(enveloped) +content_type = content_info["content_type"] +enveloped_data = content_info["content"] + +# Déchiffrement de la clés de chiffrement symétrique, si possible +decrypted_key = None +for recipient_info in enveloped_data["recipient_infos"].native: + if recipient_info["rid"]["serial_number"] == certificate.serial_number: + decrypted_key = private_key.decrypt( + recipient_info["encrypted_key"], asymmetric_padding.PKCS1v15() <1> + ) + break + +# On lève une erreur si aucune clé n'est déchiffrable +if decrypted_key is None: + raise ValueError("Aucune clé chiffrée n'est déchiffrable pour le destinataire donné.") + +# Déchiffrement du contenu en utilisant la clé symétrique fraîchement déchiffrée +def decrypt(ciphertext, key, initialization_vector) -> str: + cipher = Cipher(algorithms.AES(key), modes.CBC(initialization_vector)) <2> + decryptor = cipher.decryptor() + padded_data = decryptor.update(ciphertext) + decryptor.finalize() + unpadder = padding.PKCS7(len(key) * 8).unpadder() <3> + plaintext = unpadder.update(padded_data) + unpadder.finalize() + return plaintext.decode() + +encrypted = enveloped_data["encrypted_content_info"].native +decrypted_content = decrypt( + encrypted["encrypted_content"], + decrypted_key, + encrypted["content_encryption_algorithm"]["parameters"], +) +print("Decrypted content:", decrypted_content) +---- +<1> On suppose ici que le chiffrement de la clé symétrique a été fait avec l'algorithme +`rsaEncryption` et le padding `PKCS1v15`. Cela peut varier selon les implémentations : une autre +possibilité est `RSAES-OAEP` pour un padding plus sécurisé. +<2> On suppose ici que l'algorithme de chiffrement du contenu est AES au mode CBC. AES est +l'algorithme le plus répandu, mais d'autres modes que CBC, comme GCM, sont utilisés dans des cas +plus récents. +<3> On utilise la taille de la clé (multipliée par 8, pour passer des octects aux bits) pour +déterminer la taille du padding. Deux tailles sont classiques : 128 et 256 bits. + + +=== Avec `cryptography` + +Enfin, nous utilisons la nouvelle méthode de déchiffrement de messages PKCS #7 de la version 44.0.0 +de `cryptography`, sortie le 27 novembre 2024 : + +[source, python] +---- +from cryptography.hazmat.primitives.serialization import pkcs7 + +with open("vectors/enveloped.der", "rb") as file: + enveloped = file.read() + +decrypted = pkcs7.decrypt_der(enveloped, certificate, private_key, []) <1> +print("Decrypted content:", decrypted_content) +---- +<1> Le dernier argument est une liste d'options facultatives (voir ci-dessous). Ici, on n'en passe +aucune. + +Cette fonctionnalité simple d'utilisation se décline selon les formats des fichiers d'entrée : + +- `decrypt_der` pour for format `DER`. +- `decrypt_pem` pour le format `PEM`. +- `decrypt_smime` pour email sous format texte contenant le message PKCS #7 en pièce jointe. + +Elle s'occupe automatiquement de comparer les numéros de série des certificats dans le message +chiffré avec ceux du certificat fourni, gère les deux phases de déchiffrement, et propose des +messages d'erreur compréhensibles en cas de problème (clé privée manquante, algorithme non supporté, +etc.). + +Elle possède aussi des options pour nettoyer le texte déchiffré, afin de se rapprocher au maximum du +fonctionnement d' `openssl`. Cela permet un remplacement facile de l'utilisation d' `openssl` dans +les scripts Python. + +== Conclusion +Nous avons présenté différentes méthodes pour déchiffrer des emails chiffrés en utilisant Python. +Chacune de ces méthodes ont leurs avantages et leurs inconvénients. + +`openssl` est un exécutable facile d'accès contenant toutes les fonctionnalités cryptographiques à +portée de la ligne de commande. Cependant, pour des raisons de sécurité, il est déconseillé +d'utiliser OpenSSL directement, car il a déjà exposé des vulnérabilités par le passé. De plus, c'est +une dépendance supplémentaire, bien qu'elle soit facile à régler via Conda. + +La méthode utilisant `asn1crypto` permet de réaliser le déchiffrement 100% en Python, sans +dépendance particulière. Néanmoins, elle demande des connaissances plus avancées en cryptographie et +ne constitue pas une solution clé en main. En effet, la gestion d'autres cas d'usages de PKCS #7 +(signature, autres algorithmes) demande de la réflexion. De plus, `asn1crypto` est n'est plus +maintenue depuis fin 2022. + +Enfin, la méthode utilisant `cryptography` est la plus simple et la plus sécurisée. Elle est +installée dans une librairie maintenue, et est fiable grâce à l'utilisation de Python & Rust. C'est +la méthode que nous recommandons pour déchiffrer des emails chiffrés en Python. + +A noter néanmoins que l'intégration de cette nouvelle fonctionnalité à `cryptography` a pris du +temps à l'équipe SCIAM, entre développement et revues de code. Une contribution open-source prend du +temps, mais c'est un investissement qui en vaut la peine ! + +== Le notebook + +Vous pouvez retrouver le notebook Jupyter complet de cet article sur notre dépôt GitHub : +https://github.com/SCIAM-FR/email-decryption-demo[SCIAM-FR/email-decryption-demo]. + +== Les liens utiles +* https://cryptography.io/en/latest/[Cryptography Documentation] +* https://github.com/wbond/asn1crypto[ASN.1 Crypto Documentation] +* https://www.baeldung.com/cs/public-key-cryptography-standards[Article sur PKCS #7] \ No newline at end of file diff --git a/images/authors/quentinretourne.jpg b/images/authors/quentinretourne.jpg new file mode 100644 index 00000000..5ab285e4 Binary files /dev/null and b/images/authors/quentinretourne.jpg differ diff --git a/images/vignettes/email-decryption.png b/images/vignettes/email-decryption.png new file mode 100644 index 00000000..c8569c52 Binary files /dev/null and b/images/vignettes/email-decryption.png differ