├── .gitignore ├── LICENSE ├── README.md └── openpgp.py /.gitignore: -------------------------------------------------------------------------------- 1 | djangoapp/db.sqlite3 2 | djangoapp/db.sqlite3-journal 3 | djangoapp/static/ 4 | djangoapp/venv/ 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | 10 | # C extensions 11 | *.so 12 | 13 | # Distribution / packaging 14 | .Python 15 | env/ 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | 61 | # PyBuilder 62 | target/ 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Daniel Roesler 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # openpgp-python 2 | 3 | ### Description 4 | 5 | This is OpenPGP file parser written in python. It will dump the packet contents 6 | of a PGP/GPG formatted file to a series of json-formatted objects. These json 7 | objects are much easier to explore and import into various data analysis tools 8 | and databases. This library is able to parse the entire sks-keyserver pool of 9 | public keys. 10 | 11 | ### Table of contents 12 | 13 | * [How to use](#how-to-use) 14 | * [Command line](#command-line) 15 | * [Python library](#python-library) 16 | * [Output formats](#output-formats) 17 | * [Public-Key and Public-Subkey Packets](#public-key-and-public-subkey-packets) 18 | * [Signature Packets](#signature-packets) 19 | * [User ID Packets](#user-id-packets) 20 | * [User Attribute Packets](#user-attribute-packets) 21 | * [Roadmap](#roadmap) 22 | * [Keyserver dump](#keyserver-dump) 23 | * [Contributing](#contributing) 24 | 25 | ### How to use 26 | 27 | #### Command line 28 | 29 | ``` 30 | $ python openpgp.py --help 31 | usage: openpgp.py [-h] [-m] file [file ...] 32 | 33 | Output a pgp file's packets into rows of json-formatted objects. 34 | 35 | NOTE: Each row of output is a json object, but the whole output' 36 | itself is not a json list. 37 | 38 | Add the --merge-public-keys (-m) to roll up public keys. This is 39 | helpful if you are working with a dumped keyserver database that 40 | is just a huge list of concatenated packets (public keys, 41 | signatures, subkeys, etc.). 42 | 43 | Examples: 44 | python openpgp.py /home/me/Alice.pub 45 | python openpgp.py --merge-public-keys "/tmp/dump*.pgp" | gzip > pubkeys.json 46 | 47 | positional arguments: 48 | file the pgp file(s) 49 | 50 | optional arguments: 51 | -h, --help show this help message and exit 52 | -m, --merge-public-keys 53 | roll up public key packets 54 | ``` 55 | 56 | #### Python library 57 | 58 | ``` 59 | $ gpg --recv-key A5452207 60 | $ gpg --export A5452207 > /tmp/Alice.pub.gpg 61 | $ python 62 | Python 2.7.6 (default, Mar 22 2014, 22:59:56) 63 | [GCC 4.8.2] on linux2 64 | Type "help", "copyright", "credits" or "license" for more information. 65 | >>> from openpgp import OpenPGPFile 66 | >>> f = open("/tmp/Alice.pub.gpg") 67 | >>> packets = OpenPGPFile(f) 68 | >>> print [p['tag_name'] for p in packets] 69 | ['Public-Key', 'User ID', 'Signature', 'Signature', 'Signature', 'Public-Subkey', 'Signature'] 70 | ``` 71 | 72 | ### Output formats 73 | 74 | NOTE: These formats are the raw python dictionary formats. When using the 75 | command line, these are converted to json. 76 | 77 | #### Public-Key and Public-Subkey Packets 78 | 79 | ```python 80 | { 81 | #standard packet values 82 | "packet_format": 0 or 1, 83 | "packet_start": 0, 84 | "packet_raw": "deadbeefdeadbeefdeadbeef...", 85 | "tag_id": 6, #14 if subkey 86 | "tag_name": "Public-Key", #"Public-Subkey" if subkey 87 | "body_start": 0, 88 | "body_len": 123, 89 | 90 | #errors (if any) 91 | "error": True, 92 | "error_msg": ["Error msg 1", "Error msg 2"], 93 | 94 | #public key packet values 95 | "key_id": "deadbeefdeadbeef", 96 | "fingerprint": "deadbeefdeadbeefdeadbeefdeadbeef", 97 | "version": 3 or 4, 98 | "algo_id": 1, 99 | "algo_name": "RSA (Encrypt or Sign)", 100 | "creation_time": 1234567890, 101 | "valid_days": 30, #version 3 only 102 | 103 | #public key for openssl verification 104 | #(e.g. openssl dgst -sha1 -verify -signature ) 105 | "pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----", 106 | 107 | #RSA specific (algo_ids 1, 2, 3) 108 | "n": "deadbeef", #RSA public modulus n 109 | "e": "deadbeef", #RSA public encryption exponent e 110 | 111 | #Elgamal specific (algo_ids 16, 20) 112 | "p": "deadbeef", #Elgamal prime p 113 | "g": "deadbeef", #Elgamal group generator g 114 | 115 | #DSA specific (algo_id 17) 116 | "p": "deadbeef", #DSA prime p 117 | "q": "deadbeef", #DSA group order q (q is a prime divisor of p-1) 118 | "g": "deadbeef", #DSA group generator g 119 | "y": "deadbeef", #DSA public-key value y (= g**x mod p where x is secret) 120 | 121 | #ECDH specific (algo_id 18) 122 | #TODO 123 | 124 | #ECDSA specific (algo_id 19) 125 | "curve": "P-256", #(P-256|P-384|P-521) 126 | "x": "deadbeef", 127 | "y": "deadbeef", 128 | 129 | #packets (if the -m flag was passed in the command line) 130 | "packets": [...], #list of rolled up public key packets 131 | } 132 | ``` 133 | 134 | #### Signature Packets 135 | 136 | ```python 137 | { 138 | #standard packet values 139 | "packet_format": 0 or 1, 140 | "packet_start": 0, 141 | "packet_raw": "deadbeefdeadbeefdeadbeef...", 142 | "tag_id": 2, 143 | "tag_name": "Signature", 144 | "body_start": 5, 145 | "body_len": 423, 146 | 147 | #errors (if any) 148 | "error": True, 149 | "error_msg": ["Error msg 1", "Error msg 2"], 150 | 151 | #signature packet values 152 | "version": 3 or 4, 153 | "signature_type_id": 16, 154 | "signature_type_name": "Generic certification of a User ID and Public-Key packet", 155 | "data": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", 156 | "hash": "deadbeefdeadbeefdeadbeefdeadbeef", 157 | "hash_check": "dead", 158 | "hash_algo_id": 8, 159 | "hash_algo_name": "SHA256", 160 | "pubkey_algo_id": 1, 161 | "pubkey_algo_name": "RSA (Encrypt or Sign)", 162 | 163 | #RSA specific (algo_ids 1, 3) 164 | "signature": "deadbeefdeadbeef", 165 | 166 | #DSA and ECDSA specific (algo_id 17) 167 | "signature_r": "deadbeefdeadbeef", 168 | "signature_s": "deadbeefdeadbeef", 169 | "signature": "deadbeefdeadbeef", #ASN.1 signature for openssl verification 170 | 171 | #version 3 specific values 172 | "creation_time": 1234567890, 173 | "key_id": "deadbeefdeadbeef", 174 | 175 | #version 4 specific values 176 | "subpackets": [ 177 | { 178 | #standard subpacket values (i.e. always included) 179 | "type_id": 2, 180 | "type_name": "Signature Creation Time", 181 | "critical": True or False, 182 | "hashed": True or False, 183 | 184 | #errors (if any) 185 | "error": True, 186 | "error_msg": ["Error msg 1", "Error msg 2"], 187 | 188 | #Signature Creation Time specific (type_id 2) 189 | "creation_time": 1234567890, 190 | 191 | #Signature Expiration Time specific (type_id 3) 192 | "expiration_time": 1234567890, 193 | 194 | #Exportable Certification specific (type_id 4) 195 | "exportable": True or False, 196 | 197 | #Trust Signature specific (type_id 5) 198 | "level": 1, 199 | "amount": 255, 200 | 201 | #Regular Expression specific (type_id 6) 202 | "regex": "abc*", 203 | 204 | #Revocable specific (type_id 7) 205 | "revocable": True or False, 206 | 207 | #Key Expiration Time specific (type_id 9) 208 | "expiration_time": 1234567890, 209 | 210 | #Preferred Symmetric Algorithms specific (type_id 11) 211 | "algos": [1, 2, 3, ...], 212 | 213 | #Revocation Key specific (type_id 12) 214 | "sensitive": True or False, 215 | "pubkey_algo": 1, 216 | "fingerprint": "deadbeefdeadbeefdeadbeefdeadbeef", 217 | 218 | #Issuer specific (type_id 16) 219 | "key_id": "deadbeefdeadbeef", 220 | 221 | #Notation Data specific (type_id 20) 222 | "human_readable": True or False, 223 | "name": "", 224 | "value": "", 225 | 226 | #Preferred Hash Algorithms specific (type_id 21) 227 | "algos": [1, 2, 3, ...], 228 | 229 | #Preferred Compression Algorithms specific (type_id 22) 230 | "algos": [1, 2, 3, ...], 231 | 232 | #Key Server Preferences specific (type_id 23) 233 | "no_modify": True or False, 234 | 235 | #Preferred Key Server specific (type_id 24) 236 | "keyserver": "keys.gnupg.net", 237 | 238 | #Primary User ID specific (type_id 25) 239 | "is_primary": True or False, 240 | 241 | #Policy URI specific (type_id 26) 242 | "uri": "https://sks-keyservers.net/", 243 | 244 | #Key Flags specific (type_id 27) 245 | "can_certify": True or False, 246 | "can_sign": True or False, 247 | "can_encrypt_communication": True or False, 248 | "can_encrypt_storage": True or False, 249 | "can_authenticate": True or False, 250 | "private_might_be_split": True or False, 251 | "private_might_be_shared": True or False, 252 | 253 | #Signer's User ID specific (type_id 28) 254 | "user_id": "John Doe (johndoe1234) ", 255 | 256 | #Reason for Revocation specific (type_id 29) 257 | "code_id": 1, 258 | "code_name": "Key is superseded", 259 | "reason": "", 260 | 261 | #Features specific (type_id 30) 262 | "modification_detection": True or False, 263 | 264 | #Signature Target specific (type_id 31) 265 | "hash": "deadbeefdeadbeefdeadbeefdeadbeef", 266 | "hash_algo": 8, 267 | "pubkey_algo": 1, 268 | 269 | #Embedded Signature specific (type_id 32) 270 | "signature": {...}, #nested signature dict 271 | }, 272 | ... 273 | ], 274 | } 275 | 276 | ``` 277 | 278 | #### User ID Packets 279 | 280 | ```python 281 | { 282 | #standard packet values 283 | "packet_format": 0 or 1, 284 | "packet_start": 0, 285 | "packet_raw": "deadbeefdeadbeefdeadbeef...", 286 | "tag_id": 6, 287 | "tag_name": "User ID", 288 | "body_start": 0, 289 | "body_len": 123, 290 | 291 | #errors (if any) 292 | "error": True, 293 | "error_msg": ["Error msg 1", "Error msg 2"], 294 | 295 | #User ID specific fields 296 | "user_id": "John Doe (johndoe1234) ", 297 | } 298 | ``` 299 | 300 | #### User Attribute Packets 301 | 302 | ```python 303 | { 304 | #standard packet values 305 | "packet_format": 0 or 1, 306 | "packet_start": 0, 307 | "packet_raw": "deadbeefdeadbeefdeadbeef...", 308 | "tag_id": 17, 309 | "tag_name": "User Attribute", 310 | "body_start": 0, 311 | "body_len": 123, 312 | 313 | #errors (if any) 314 | "error": True, 315 | "error_msg": ["Error msg 1", "Error msg 2"], 316 | 317 | #User Attribute specific fields 318 | "subpackets": [ 319 | { 320 | #standard subpacket values 321 | "type_id": 1, 322 | "type_name": "Image", 323 | 324 | #errors (if any) 325 | "error": True, 326 | "error_msg": ["Error msg 1", "Error msg 2"], 327 | 328 | #image specific values 329 | "version": 1, 330 | "encoding": "JPEG", 331 | "image": "", 332 | }, 333 | ... 334 | ], 335 | } 336 | ``` 337 | 338 | ### Roadmap 339 | 340 | * ~~Parsing raw packets.~~ 341 | * ~~Parsing [ASCII armored packets](https://tools.ietf.org/html/rfc4880#section-6.2)~~ 342 | * ~~Parsing publickey related packets.~~ 343 | * ~~[Signature Packet (Tag 2)](https://tools.ietf.org/html/rfc4880#section-5.2)~~ 344 | * ~~[Public-Key Packet (Tag 6)](https://tools.ietf.org/html/rfc4880#section-5.5.1.1)~~ 345 | * ~~RSA~~ 346 | * ~~Elgamal~~ 347 | * ~~DSA~~ 348 | * ~~ECDH~~ 349 | * ~~ECDSA~~ 350 | * DH 351 | * ~~EdDSA~~ 352 | * ~~[Public-Subkey Packet (Tag 14)](https://tools.ietf.org/html/rfc4880#section-5.5.1.2)~~ 353 | * ~~[User ID Packet (Tag 13)](https://tools.ietf.org/html/rfc4880#section-5.11)~~ 354 | * ~~[User Attribute Packet (Tag 17)](https://tools.ietf.org/html/rfc4880#section-5.12)~~ 355 | * Parsing non-publickey related packets. 356 | * [Public-Key Encrypted Session Key Packets (Tag 1)](https://tools.ietf.org/html/rfc4880#section-5.1) 357 | * [Symmetric-Key Encrypted Session Key Packets (Tag 3)](https://tools.ietf.org/html/rfc4880#section-5.3) 358 | * ~~[One-Pass Signature Packets (Tag 4)](https://tools.ietf.org/html/rfc4880#section-5.4)~~ 359 | * [Secret-Key Packet (Tag 5)](https://tools.ietf.org/html/rfc4880#section-5.5.1.3) 360 | * [Secret-Subkey Packet (Tag 7)](https://tools.ietf.org/html/rfc4880#section-5.5.1.4) 361 | * ~~[Compressed Data Packet (Tag 8)](https://tools.ietf.org/html/rfc4880#section-5.6)~~ 362 | * [Symmetrically Encrypted Data Packet (Tag 9)](https://tools.ietf.org/html/rfc4880#section-5.7) 363 | * [Marker Packet (Obsolete Literal Packet) (Tag 10)](https://tools.ietf.org/html/rfc4880#section-5.8) 364 | * ~~[Literal Data Packet (Tag 11)](https://tools.ietf.org/html/rfc4880#section-5.9)~~ 365 | * [Trust Packet (Tag 12)](https://tools.ietf.org/html/rfc4880#section-5.10) 366 | * [Sym. Encrypted Integrity Protected Data Packet (Tag 18)](https://tools.ietf.org/html/rfc4880#section-5.13) 367 | * [Modification Detection Code Packet (Tag 19)](https://tools.ietf.org/html/rfc4880#section-5.14) 368 | * Generating raw packets 369 | * [Public-Key Encrypted Session Key Packets (Tag 1)](https://tools.ietf.org/html/rfc4880#section-5.1) 370 | * ~~[Signature Packet (Tag 2)](https://tools.ietf.org/html/rfc4880#section-5.2)~~ 371 | * [Symmetric-Key Encrypted Session Key Packets (Tag 3)](https://tools.ietf.org/html/rfc4880#section-5.3) 372 | * [One-Pass Signature Packets (Tag 4)](https://tools.ietf.org/html/rfc4880#section-5.4) 373 | * [Secret-Key Packet (Tag 5)](https://tools.ietf.org/html/rfc4880#section-5.5.1.3) 374 | * [Public-Key Packet (Tag 6)](https://tools.ietf.org/html/rfc4880#section-5.5.1.1) 375 | * ~~RSA~~ 376 | * Elgamal 377 | * DSA 378 | * ECDH 379 | * ECDSA 380 | * DH 381 | * EdDSA 382 | * [Secret-Subkey Packet (Tag 7)](https://tools.ietf.org/html/rfc4880#section-5.5.1.4) 383 | * [Compressed Data Packet (Tag 8)](https://tools.ietf.org/html/rfc4880#section-5.6) 384 | * [Symmetrically Encrypted Data Packet (Tag 9)](https://tools.ietf.org/html/rfc4880#section-5.7) 385 | * [Marker Packet (Obsolete Literal Packet) (Tag 10)](https://tools.ietf.org/html/rfc4880#section-5.8) 386 | * [Literal Data Packet (Tag 11)](https://tools.ietf.org/html/rfc4880#section-5.9) 387 | * [Trust Packet (Tag 12)](https://tools.ietf.org/html/rfc4880#section-5.10) 388 | * ~~[User ID Packet (Tag 13)](https://tools.ietf.org/html/rfc4880#section-5.11)~~ 389 | * ~~[Public-Subkey Packet (Tag 14)](https://tools.ietf.org/html/rfc4880#section-5.5.1.2)~~ 390 | * ~~[User Attribute Packet (Tag 17)](https://tools.ietf.org/html/rfc4880#section-5.12)~~ 391 | * [Sym. Encrypted Integrity Protected Data Packet (Tag 18)](https://tools.ietf.org/html/rfc4880#section-5.13) 392 | * [Modification Detection Code Packet (Tag 19)](https://tools.ietf.org/html/rfc4880#section-5.14) 393 | * Generating ASCII armored packets 394 | * Tests 395 | 396 | #### OpenSSL Signature Verification 397 | 398 | One of the goals of this project is to be able to verify signatures using only 399 | openssl. You can do this by dumping the public key `pem` key to a file, the 400 | signature hex to a file, and the signature data to a file. 401 | 402 | NOTE: This currently only works with DSA and RSA keys. 403 | 404 | Here's an example of verifying my self-signature on my first user id: 405 | 406 | ```sh 407 | $ gpg --recv-key 72EFEE3D 408 | $ gpg --export 72EFEE3D > test.asc 409 | $ python openpgp.py -m test.asc > test.json 410 | Parsing test.asc...Done 411 | Dumping public keys...Done 412 | $ python -c 'import sys,json; sys.stdout.write(json.loads(open("test.json").read())["pem"])' > test.pub 413 | $ python -c 'import sys,json; sys.stdout.write(json.loads(open("test.json").read())["packets"][1]["data"].decode("hex"))' > test.data 414 | $ python -c 'import sys,json; sys.stdout.write(json.loads(open("test.json").read())["packets"][1]["signature"].decode("hex"))' > test.sig 415 | $ openssl dgst -sha1 -verify test.pub -signature test.sig test.data 416 | Verified OK 417 | ``` 418 | 419 | #### Keyserver dump 420 | 421 | This is how you can load a keyserver dump into elasticsearch. 422 | 423 | ```sh 424 | #download openpgp.py 425 | mkdir ~/opengpg-python 426 | cd ~/openpgp-python 427 | wget https://raw.githubusercontent.com/diafygi/openpgp-python/master/openpgp.py > openpgp.py 428 | 429 | #download the latest keyserver dump 430 | mkdir ~/dump 431 | cd ~/dump 432 | wget -c -r -p -e robots=off --timestamping --level=1 --cut-dirs=3 \ 433 | --no-host-directories http://keyserver.mattrude.com/dump/current/ 434 | 435 | #Parse keyserver dump to json gzip files (split every 1000 lines) 436 | ls -1 ~/dump/*.pgp | \ 437 | xargs -I % sh -c "python ~/openpgp-python/openpgp.py --merge-public-keys '%' | \ 438 | split -l 1000 -d --filter 'gzip -9 > $FILE.gz' - '%.json.'" 439 | 440 | #Bulk index each gzip file into elasticsearch 441 | ls -1 ~/dump/*.json.*.gz | \ 442 | xargs -I % sh -c "zcat '%' | \ 443 | sed '0~1 s/^/{ \"index\" : { \"_index\" : \"keyserver1\", \"_type\" : \"key\" } }\n/' | \ 444 | curl -X POST --data-binary @- http://localhost:9200/_bulk | \ 445 | { cat -; echo ''; } >> ~/results.log" 446 | ``` 447 | 448 | ### Contributing 449 | 450 | I'd love pull requests adding support for unsupported packets types. I'd also 451 | love pull requests adding tests. File bug reports and feature requests in the 452 | issue tracker. Thanks! 453 | 454 | -------------------------------------------------------------------------------- /openpgp.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import re 5 | import bz2 6 | import math 7 | import zlib 8 | import base64 9 | import hashlib 10 | import tempfile 11 | import cStringIO 12 | 13 | class OpenPGPFile(list): 14 | """ 15 | A list of parsed packets in an OpenPGP file. I wrote this to have a better 16 | understanding of the OpenPGP format. It is designed to be very readable, 17 | use low memory, and use Python's built-in list and dict objects. 18 | 19 | Object format = [ 20 | { 21 | #standard packet values 22 | "packet_format": 0 or 1, 23 | "packet_start": 123, 24 | "packet_raw": "deadbeefdeadbeefdeadbeef...", 25 | "tag_id": 2, 26 | "tag_name": "Signature", 27 | "body_start": 0, 28 | "body_len": 423, 29 | 30 | #errors (if any) 31 | "error": True, 32 | "error_msg": ["Error msg 1", "Error msg 2"], 33 | 34 | #packet specific keys (see each read_* method for format) 35 | ... 36 | }, 37 | ... 38 | ] 39 | """ 40 | rawfile = None 41 | 42 | def __init__(self, fileobj_or_list): 43 | 44 | #read the file and load the list 45 | if hasattr(fileobj_or_list, "read"): 46 | super(OpenPGPFile, self).__init__() 47 | self.rawfile = fileobj_or_list 48 | 49 | #try and load the packets as-is 50 | try: 51 | self.read_packets() 52 | 53 | #couldn't load the packets, so try reading them as armored text 54 | except ValueError: 55 | self.rawfile = self.armor_to_bytes(fileobj_or_list) 56 | self.read_packets() 57 | 58 | #just set the given list 59 | else: 60 | super(OpenPGPFile, self).__init__(fileobj_or_list) 61 | 62 | def __getslice__(self, *args): 63 | result = super(OpenPGPFile, self).__getslice__(*args) 64 | result = OpenPGPFile(result) 65 | result.rawfile = self.rawfile 66 | return result 67 | 68 | def read_signature(self, body_start, body_len, msg_body=""): 69 | """ 70 | Specification: 71 | https://tools.ietf.org/html/rfc4880#section-5.2 72 | 73 | Signature Types: 74 | ID Signature type 75 | -- --------- 76 | 0 (0x00) - Signature of a binary document 77 | 1 (0x01) - Signature of a canonical text document 78 | 2 (0x02) - Standalone signature 79 | 16 (0x10) - Generic certification of a User ID and Public-Key packet 80 | 17 (0x11) - Persona certification of a User ID and Public-Key packet 81 | 18 (0x12) - Casual certification of a User ID and Public-Key packet 82 | 19 (0x13) - Positive certification of a User ID and Public-Key packet 83 | 24 (0x18) - Subkey Binding Signature 84 | 25 (0x19) - Primary Key Binding Signature 85 | 31 (0x1F) - Signature directly on a key 86 | 32 (0x20) - Key revocation signature 87 | 40 (0x28) - Subkey revocation signature 88 | 48 (0x30) - Certification revocation signature 89 | 64 (0x40) - Timestamp signature 90 | 80 (0x50) - Third-Party Confirmation signature 91 | 92 | Public Key Algorithms: 93 | ID Algorithm 94 | -- --------- 95 | 1 - RSA (Encrypt or Sign) 96 | 2 - RSA Encrypt-Only 97 | 3 - RSA Sign-Only 98 | 16 - Elgamal (Encrypt-Only) 99 | 17 - DSA (Digital Signature Algorithm) 100 | 18 - ECDH public key algorithm 101 | 19 - ECDSA public key algorithm 102 | 20 - Reserved (formerly Elgamal Encrypt or Sign) 103 | 21 - Reserved for Diffie-Hellman (X9.42, as defined for IETF-S/MIME) 104 | 100 to 110 - Private/Experimental algorithm 105 | 106 | Hash Algorithms: 107 | ID Algorithm 108 | -- --------- 109 | 1 - MD5 110 | 2 - SHA1 111 | 3 - RIPEMD160 112 | 4 - Reserved 113 | 5 - Reserved 114 | 6 - Reserved 115 | 7 - Reserved 116 | 8 - SHA256 117 | 9 - SHA384 118 | 10 - SHA512 119 | 11 - SHA224 120 | 100 to 110 - Private/Experimental algorithm 121 | 122 | Subpacket Types: 123 | ID Type 124 | -- --------- 125 | 0 - Reserved 126 | 1 - Reserved 127 | 2 - Signature Creation Time 128 | 3 - Signature Expiration Time 129 | 4 - Exportable Certification 130 | 5 - Trust Signature 131 | 6 - Regular Expression 132 | 7 - Revocable 133 | 8 - Reserved 134 | 9 - Key Expiration Time 135 | 10 - Placeholder for backward compatibility 136 | 11 - Preferred Symmetric Algorithms 137 | 12 - Revocation Key 138 | 13 - Reserved 139 | 14 - Reserved 140 | 15 - Reserved 141 | 16 - Issuer 142 | 17 - Reserved 143 | 18 - Reserved 144 | 19 - Reserved 145 | 20 - Notation Data 146 | 21 - Preferred Hash Algorithms 147 | 22 - Preferred Compression Algorithms 148 | 23 - Key Server Preferences 149 | 24 - Preferred Key Server 150 | 25 - Primary User ID 151 | 26 - Policy URI 152 | 27 - Key Flags 153 | 28 - Signer's User ID 154 | 29 - Reason for Revocation 155 | 30 - Features 156 | 31 - Signature Target 157 | 32 - Embedded Signature 158 | 100 To 110 - Private or experimental 159 | 160 | Revocation Codes: 161 | ID Reason 162 | -- --------- 163 | 0 - No reason specified (key revocations or cert revocations) 164 | 1 - Key is superseded (key revocations) 165 | 2 - Key material has been compromised (key revocations) 166 | 3 - Key is retired and no longer used (key revocations) 167 | 32 - User ID information is no longer valid (cert revocations) 168 | 100-110 - Private Use 169 | 170 | Return Format: 171 | { 172 | #standard packet values 173 | "tag_id": 2, 174 | "tag_name": "Signature", 175 | "body_start": 0, 176 | "body_len": 123, 177 | 178 | #errors (if any) 179 | "error": True, 180 | "error_msg": ["Error msg 1", "Error msg 2"], 181 | 182 | #signature packet values 183 | "version": 3 or 4, 184 | "signature_type_id": 16, 185 | "signature_type_name": "Generic certification of a User ID and Public-Key packet", 186 | "data": "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef", 187 | "hash": "deadbeefdeadbeefdeadbeefdeadbeef", 188 | "hash_check": "dead", 189 | "hash_algo_id": 8, 190 | "hash_algo_name": "SHA256", 191 | "pubkey_algo_id": 1, 192 | "pubkey_algo_name": "RSA (Encrypt or Sign)", 193 | 194 | #RSA specific (algo_ids 1, 3) 195 | "signature": "deadbeefdeadbeef", 196 | 197 | #DSA and ECDSA specific (algo_id 17) 198 | "signature_r": "deadbeefdeadbeef", 199 | "signature_s": "deadbeefdeadbeef", 200 | "signature": "deadbeefdeadbeef", #this is the combined signature which 201 | #can be used for openssl verification 202 | 203 | #version 3 specific values 204 | "creation_time": 1234567890, 205 | "key_id": "deadbeefdeadbeef", 206 | 207 | #version 4 specific values 208 | "subpackets": [ 209 | { 210 | #standard subpacket values (i.e. always included) 211 | "type_id": 2, 212 | "type_name": "Signature Creation Time", 213 | "critical": True or False, 214 | "hashed": True or False, 215 | 216 | #errors (if any) 217 | "error": True, 218 | "error_msg": ["Error msg 1", "Error msg 2"], 219 | 220 | #Signature Creation Time specific (type_id 2) 221 | "creation_time": 1234567890, 222 | 223 | #Signature Expiration Time specific (type_id 3) 224 | "expiration_time": 1234567890, 225 | 226 | #Exportable Certification specific (type_id 4) 227 | "exportable": True or False, 228 | 229 | #Trust Signature specific (type_id 5) 230 | "level": 1, 231 | "amount": 255, 232 | 233 | #Regular Expression specific (type_id 6) 234 | "regex": "abc*", 235 | 236 | #Revocable specific (type_id 7) 237 | "revocable": True or False, 238 | 239 | #Key Expiration Time specific (type_id 9) 240 | "expiration_time": 1234567890, 241 | 242 | #Preferred Symmetric Algorithms specific (type_id 11) 243 | "algos": [1, 2, 3, ...], 244 | 245 | #Revocation Key specific (type_id 12) 246 | "sensitive": True or False, 247 | "pubkey_algo": 1, 248 | "fingerprint": "deadbeefdeadbeefdeadbeefdeadbeef", 249 | 250 | #Issuer specific (type_id 16) 251 | "key_id": "deadbeefdeadbeef", 252 | 253 | #Notation Data specific (type_id 20) 254 | "human_readable": True or False, 255 | "name": "", 256 | "value": "", 257 | 258 | #Preferred Hash Algorithms specific (type_id 21) 259 | "algos": [1, 2, 3, ...], 260 | 261 | #Preferred Compression Algorithms specific (type_id 22) 262 | "algos": [1, 2, 3, ...], 263 | 264 | #Key Server Preferences specific (type_id 23) 265 | "no_modify": True or False, 266 | 267 | #Preferred Key Server specific (type_id 24) 268 | "keyserver": "keys.gnupg.net", 269 | 270 | #Primary User ID specific (type_id 25) 271 | "is_primary": True or False, 272 | 273 | #Policy URI specific (type_id 26) 274 | "uri": "https://sks-keyservers.net/", 275 | 276 | #Key Flags specific (type_id 27) 277 | "can_certify": True or False, 278 | "can_sign": True or False, 279 | "can_encrypt_communication": True or False, 280 | "can_encrypt_storage": True or False, 281 | "can_authenticate": True or False, 282 | "private_might_be_split": True or False, 283 | "private_might_be_shared": True or False, 284 | 285 | #Signer's User ID specific (type_id 28) 286 | "user_id": "John Doe (johndoe1234) ", 287 | 288 | #Reason for Revocation specific (type_id 29) 289 | "code_id": 1, 290 | "code_name": "Key is superseded", 291 | "reason": "", 292 | 293 | #Features specific (type_id 30) 294 | "modification_detection": True or False, 295 | 296 | #Signature Target specific (type_id 31) 297 | "hash": "deadbeefdeadbeefdeadbeefdeadbeef", 298 | "hash_algo": 8, 299 | "pubkey_algo": 1, 300 | 301 | #Embedded Signature specific (type_id 32) 302 | "signature": {...}, #nested signature dict 303 | }, 304 | ... 305 | ], 306 | } 307 | """ 308 | result = { 309 | "tag_id": 2, 310 | "tag_name": "Signature", 311 | "body_start": body_start, 312 | "body_len": body_len, 313 | } 314 | 315 | #version 316 | self.rawfile.seek(body_start) 317 | version = ord(self.rawfile.read(1)) 318 | if version not in [3, 4]: 319 | result['error'] = True 320 | result.setdefault("error_msg", []).append("Signature version is invalid ({}).".format(version)) 321 | return result 322 | result['version'] = version 323 | 324 | #signature body length (version 3 only) 325 | if version == 3: 326 | sig_header_len = ord(self.rawfile.read(1)) 327 | if sig_header_len != 5: 328 | result['error'] = True 329 | result.setdefault("error_msg", []).append("Signature body ({} bytes) not 5 bytes long.".format(sig_header_len)) 330 | return result 331 | 332 | #signature type 333 | signature_type_id = ord(self.rawfile.read(1)) 334 | try: 335 | signature_type_name = { 336 | 0: "Signature of a binary document", 337 | 1: "Signature of a canonical text document", 338 | 2: "Standalone signature", 339 | 16: "Generic certification", 340 | 17: "Persona certification", 341 | 18: "Casual certification", 342 | 19: "Positive certification", 343 | 24: "Subkey Binding", 344 | 25: "Primary Key Binding", 345 | 31: "Signature directly on a key", 346 | 32: "Key revocation", 347 | 40: "Subkey revocation", 348 | 48: "Certification revocation", 349 | 64: "Timestamp", 350 | 80: "Third-Party Confirmation", 351 | }[signature_type_id] 352 | except KeyError: 353 | signature_type_name = "Unknown" 354 | result['error'] = True 355 | result.setdefault("error_msg", []).append("Signature type ({}) not recognized.".format(signature_type_id)) 356 | result['signature_type_id'] = signature_type_id 357 | result['signature_type_name'] = signature_type_name 358 | 359 | #creation time (version 3 only) 360 | if version == 3: 361 | creation_bytes = self.rawfile.read(4) 362 | creation_time = int(creation_bytes.encode('hex'), 16) 363 | result['creation_time'] = creation_time 364 | 365 | #signer key_id (version 3 only) 366 | if version == 3: 367 | key_id = self.rawfile.read(8).encode("hex") 368 | result['key_id'] = key_id 369 | 370 | #public key algorithm 371 | pubkey_algo_id = ord(self.rawfile.read(1)) 372 | try: 373 | pubkey_algo_name = { 374 | 1: "RSA (Encrypt or Sign)", 375 | 2: "RSA Encrypt-Only", 376 | 3: "RSA Sign-Only", 377 | 16: "Elgamal (Encrypt-Only)", 378 | 17: "DSA (Digital Signature Algorithm)", 379 | 18: "ECDH public key algorithm", 380 | 19: "ECDSA public key algorithm", 381 | 20: "Reserved (formerly Elgamal Encrypt or Sign)", 382 | 21: "Reserved for Diffie-Hellman (X9.42, as defined for IETF-S/MIME)", 383 | 22: "EdDSA public key algorithm", 384 | 100: "Private or experimental", 385 | 101: "Private or experimental", 386 | 102: "Private or experimental", 387 | 103: "Private or experimental", 388 | 104: "Private or experimental", 389 | 105: "Private or experimental", 390 | 106: "Private or experimental", 391 | 107: "Private or experimental", 392 | 108: "Private or experimental", 393 | 109: "Private or experimental", 394 | 110: "Private or experimental", 395 | }[pubkey_algo_id] 396 | except KeyError: 397 | pubkey_algo_name = "Unknown" 398 | result['error'] = True 399 | result.setdefault("error_msg", []).append("Public-Key algorithm ({}) not recognized.".format(pubkey_algo_id)) 400 | result['pubkey_algo_id'] = pubkey_algo_id 401 | result['pubkey_algo_name'] = pubkey_algo_name 402 | 403 | #hash algorithm 404 | hash_algo_id = ord(self.rawfile.read(1)) 405 | try: 406 | hash_algo_name = { 407 | 1: "MD5", 408 | 2: "SHA1", 409 | 3: "RIPEMD160", 410 | 4: "Reserved", 411 | 5: "Reserved", 412 | 6: "Reserved", 413 | 7: "Reserved", 414 | 8: "SHA256", 415 | 9: "SHA384", 416 | 10: "SHA512", 417 | 11: "SHA224", 418 | }[hash_algo_id] 419 | except KeyError: 420 | hash_algo_name = "Unknown" 421 | result['error'] = True 422 | result.setdefault("error_msg", []).append("Hash algorithm ({}) not recognized.".format(hash_algo_id)) 423 | result['hash_algo_id'] = hash_algo_id 424 | result['hash_algo_name'] = hash_algo_name 425 | 426 | #subpackets (version 4 only) 427 | if version == 4: 428 | 429 | #hashed subpackets length 430 | hashed_subpacket_len_bytes = self.rawfile.read(2) 431 | hashed_subpacket_len = int(hashed_subpacket_len_bytes.encode('hex'), 16) 432 | hashed_subpacket_end = self.rawfile.tell() + hashed_subpacket_len 433 | 434 | #make sure the section length is not over the end point 435 | if hashed_subpacket_end > body_start + body_len: 436 | result['error'] = True 437 | result.setdefault("error_msg", []).append("Hashed subpacket section overflows the overall length.") 438 | hashed_subpacket_end = body_start + body_len 439 | 440 | #hashed subpackets 441 | subpackets_raw = [] 442 | while self.rawfile.tell() < hashed_subpacket_end: 443 | 444 | #one byte length 445 | first_octet = ord(self.rawfile.read(1)) 446 | if first_octet < 192: 447 | subpacket_len = first_octet 448 | 449 | #two bytes length 450 | elif first_octet >= 192 and first_octet < 255: 451 | second_octet = ord(self.rawfile.read(1)) 452 | subpacket_len = ((first_octet - 192) << 8) + second_octet + 192 453 | 454 | #four bytes length 455 | elif first_octet == 255: 456 | four_bytes = self.rawfile.read(4) 457 | subpacket_len = int(four_bytes.encode('hex'), 16) 458 | 459 | #make sure the subpacket length is not over the end point 460 | if self.rawfile.tell() + subpacket_len > hashed_subpacket_end: 461 | result['error'] = True 462 | result.setdefault("error_msg", []).append("Hashed subpacket length overflows the overall length.") 463 | subpacket_len = hashed_subpacket_end - self.rawfile.tell() 464 | 465 | #save the position and length of the subpacket 466 | subpacket_start = self.rawfile.tell() 467 | self.rawfile.seek(subpacket_start + subpacket_len) 468 | subpackets_raw.append([True, subpacket_start, subpacket_len]) 469 | 470 | #hashed subpackets length 471 | unhashed_subpacket_len_bytes = self.rawfile.read(2) 472 | unhashed_subpacket_len = int(unhashed_subpacket_len_bytes.encode('hex'), 16) 473 | unhashed_subpacket_end = self.rawfile.tell() + unhashed_subpacket_len 474 | 475 | #make sure the section length is not over the end point 476 | if unhashed_subpacket_end > body_start + body_len: 477 | result['error'] = True 478 | result.setdefault("error_msg", []).append("Unhashed subpacket section overflows the overall length.") 479 | unhashed_subpacket_end = body_start + body_len 480 | 481 | #hashed subpackets 482 | while self.rawfile.tell() < unhashed_subpacket_end: 483 | 484 | #one byte length 485 | first_octet = ord(self.rawfile.read(1)) 486 | if first_octet < 192: 487 | subpacket_len = first_octet 488 | 489 | #two bytes length 490 | elif first_octet >= 192 and first_octet < 255: 491 | second_octet = ord(self.rawfile.read(1)) 492 | subpacket_len = ((first_octet - 192) << 8) + second_octet + 192 493 | 494 | #four bytes length 495 | elif first_octet == 255: 496 | four_bytes = self.rawfile.read(4) 497 | subpacket_len = int(four_bytes.encode('hex'), 16) 498 | 499 | #make sure the subpacket length is not over the end point 500 | if self.rawfile.tell() + subpacket_len > unhashed_subpacket_end: 501 | result['error'] = True 502 | result.setdefault("error_msg", []).append("Unhashed subpacket length overflows the overall length.") 503 | subpacket_len = unhashed_subpacket_end - self.rawfile.tell() 504 | 505 | #save the position and length of the subpacket 506 | subpacket_start = self.rawfile.tell() 507 | self.rawfile.seek(subpacket_start + subpacket_len) 508 | subpackets_raw.append([False, subpacket_start, subpacket_len]) 509 | 510 | #parse subpackets 511 | result['subpackets'] = [] 512 | for is_hashed, subpacket_start, subpacket_len in subpackets_raw: 513 | 514 | #create the base subpacket dict 515 | subpacket = {"hashed": is_hashed} 516 | self.rawfile.seek(subpacket_start) 517 | first_octet = ord(self.rawfile.read(1)) 518 | 519 | #critical flag 520 | critical = first_octet >> 7 == 1 521 | subpacket['critical'] = critical 522 | 523 | #subpacket type 524 | type_id = first_octet & 0x7F 525 | try: 526 | type_name = { 527 | 2: "Signature Creation Time", 528 | 3: "Signature Expiration Time", 529 | 4: "Exportable Certification", 530 | 5: "Trust Signature", 531 | 6: "Regular Expression", 532 | 7: "Revocable", 533 | 9: "Key Expiration Time", 534 | 10: "Placeholder for backward compatibility", 535 | 11: "Preferred Symmetric Algorithms", 536 | 12: "Revocation Key", 537 | 16: "Issuer", 538 | 20: "Notation Data", 539 | 21: "Preferred Hash Algorithms", 540 | 22: "Preferred Compression Algorithms", 541 | 23: "Key Server Preferences", 542 | 24: "Preferred Key Server", 543 | 25: "Primary User ID", 544 | 26: "Policy URI", 545 | 27: "Key Flags", 546 | 28: "Signer's User ID", 547 | 29: "Reason for Revocation", 548 | 30: "Features", 549 | 31: "Signature Target", 550 | 32: "Embedded Signature", 551 | 100: "Private or experimental", 552 | 101: "Private or experimental", 553 | 102: "Private or experimental", 554 | 103: "Private or experimental", 555 | 104: "Private or experimental", 556 | 105: "Private or experimental", 557 | 106: "Private or experimental", 558 | 107: "Private or experimental", 559 | 108: "Private or experimental", 560 | 109: "Private or experimental", 561 | 110: "Private or experimental", 562 | }[type_id] 563 | except KeyError: 564 | result['error'] = True 565 | result.setdefault("error_msg", []).append("Subpacket type ({}) not recognized.".format(type_id)) 566 | subpacket['error'] = True 567 | subpacket.setdefault("error_msg", []).append("Subpacket type ({}) not recognized.".format(type_id)) 568 | continue 569 | subpacket['type_id'] = type_id 570 | subpacket['type_name'] = type_name 571 | 572 | #Signature Creation Time 573 | if type_id == 2: 574 | creation_bytes = self.rawfile.read(4) 575 | creation_time = int(creation_bytes.encode('hex'), 16) 576 | subpacket['creation_time'] = creation_time 577 | 578 | #Signature Expiration Time 579 | elif type_id == 3: 580 | expiration_bytes = self.rawfile.read(4) 581 | expiration_time = int(expiration_bytes.encode('hex'), 16) 582 | subpacket['expiration_time'] = expiration_time 583 | 584 | #Exportable Certification 585 | elif type_id == 4: 586 | exportable = ord(self.rawfile.read(1)) == 1 587 | subpacket['exportable'] = exportable 588 | 589 | #Trust Signature 590 | elif type_id == 5: 591 | trust_level = ord(self.rawfile.read(1)) 592 | trust_amount = ord(self.rawfile.read(1)) 593 | subpacket['level'] = trust_level 594 | subpacket['amount'] = trust_amount 595 | 596 | #Regular Expression 597 | elif type_id == 6: 598 | regex_str = self.rawfile.read(subpacket_len - 1) 599 | subpacket['regex'] = regex_str 600 | 601 | #Revocable 602 | elif type_id == 7: 603 | revocable = ord(self.rawfile.read(1)) == 1 604 | subpacket['revocable'] = revocable 605 | 606 | #Key Expiration Time 607 | elif type_id == 9: 608 | expiration_bytes = self.rawfile.read(4) 609 | expiration_time = int(expiration_bytes.encode('hex'), 16) 610 | subpacket['expiration_time'] = expiration_time 611 | 612 | #Preferred Symmetric Algorithms 613 | elif type_id == 11: 614 | algo_bytes = self.rawfile.read(subpacket_len - 1) 615 | subpacket['algos'] = [] 616 | for a in algo_bytes: 617 | subpacket['algos'].append(ord(a)) 618 | 619 | #Revocation Key 620 | elif type_id == 12: 621 | 622 | #revocation class 623 | revoke_class = ord(self.rawfile.read(1)) 624 | if revoke_class >> 7 != 1: 625 | result['error'] = True 626 | result.setdefault("error_msg", []).append("Revoke class must start with a 1 bit.") 627 | subpacket['error'] = True 628 | subpacket.setdefault("error_msg", []).append("Revoke class must start with a 1 bit.") 629 | sensitive = revoke_class & 0x40 >> 6 == 1 630 | subpacket['sensitive'] = sensitive 631 | 632 | #public key algorithm 633 | pubkey_algo = ord(self.rawfile.read(1)) 634 | subpacket['pubkey_algo'] = pubkey_algo 635 | 636 | #revocation key fingerprint 637 | fingerprint = self.rawfile.read(20).encode("hex") 638 | subpacket['fingerprint'] = fingerprint 639 | 640 | #Issuer 641 | elif type_id == 16: 642 | key_id = self.rawfile.read(8).encode("hex") 643 | subpacket['key_id'] = key_id 644 | 645 | #Notation Data 646 | elif type_id == 20: 647 | 648 | #flags (only human_readable is defined) 649 | flags = int(self.rawfile.read(4).encode('hex'), 16) 650 | human_readable = flags >> 31 == 1 651 | subpacket['human_readable'] = human_readable 652 | 653 | #name and value 654 | name_len = int(self.rawfile.read(2).encode('hex'), 16) 655 | value_len = int(self.rawfile.read(2).encode('hex'), 16) 656 | if self.rawfile.tell() + name_len > subpacket_start + subpacket_len: 657 | result['error'] = True 658 | result.setdefault("error_msg", []).append("Subpacket name overflows the overall length.") 659 | subpacket['error'] = True 660 | subpacket.setdefault("error_msg", []).append("Subpacket name overflows the overall length.") 661 | name_len = max(0, subpacket_len - (self.rawfile.tell() - subpacket_start)) 662 | value_len = 0 663 | elif self.rawfile.tell() + name_len + value_len > subpacket_start + subpacket_len: 664 | result['error'] = True 665 | result.setdefault("error_msg", []).append("Subpacket value overflows the overall length.") 666 | subpacket['error'] = True 667 | subpacket.setdefault("error_msg", []).append("Subpacket value overflows the overall length.") 668 | value_len = max(0, subpacket_len - (self.rawfile.tell() - subpacket_start)) 669 | subpacket['name'] = self.rawfile.read(name_len) 670 | subpacket['value'] = self.rawfile.read(value_len) 671 | 672 | #Preferred Hash Algorithms 673 | elif type_id == 21: 674 | algo_bytes = self.rawfile.read(subpacket_len - 1) 675 | subpacket['algos'] = [] 676 | for a in algo_bytes: 677 | subpacket['algos'].append(ord(a)) 678 | 679 | #Preferred Compression Algorithms 680 | elif type_id == 22: 681 | algo_bytes = self.rawfile.read(subpacket_len - 1) 682 | subpacket['algos'] = [] 683 | for a in algo_bytes: 684 | subpacket['algos'].append(ord(a)) 685 | 686 | #Key Server Preferences (only first bit is defined) 687 | elif type_id == 23: 688 | no_modify = ord(self.rawfile.read(1)) & 0x80 == 0x80 689 | subpacket['no_modify'] = no_modify 690 | 691 | #Preferred Key Server 692 | elif type_id == 24: 693 | keyserver = self.rawfile.read(subpacket_len - 1) 694 | subpacket['keyserver'] = keyserver 695 | 696 | #Primary User ID 697 | elif type_id == 25: 698 | is_primary = ord(self.rawfile.read(1)) == 1 699 | subpacket['is_primary'] = is_primary 700 | 701 | #Policy URI 702 | elif type_id == 26: 703 | policy_uri = self.rawfile.read(subpacket_len - 1) 704 | subpacket['uri'] = policy_uri 705 | 706 | #Key Flags (only first octet has defined flags) 707 | elif type_id == 27: 708 | flags = ord(self.rawfile.read(1)) 709 | subpacket['can_certify'] = flags & 0x01 == 0x01 710 | subpacket['can_sign'] = flags & 0x02 == 0x02 711 | subpacket['can_encrypt_communication'] = flags & 0x04 == 0x04 712 | subpacket['can_encrypt_storage'] = flags & 0x08 == 0x08 713 | subpacket['can_authenticate'] = flags & 0x20 == 0x20 714 | subpacket['private_might_be_split'] = flags & 0x10 == 0x10 715 | subpacket['private_might_be_shared'] = flags & 0x80 == 0x80 716 | 717 | #Signer's User ID 718 | elif type_id == 28: 719 | user_id = self.rawfile.read(subpacket_len - 1) 720 | subpacket['user_id'] = user_id 721 | 722 | #Reason for Revocation 723 | elif type_id == 29: 724 | 725 | #revocation code 726 | code_id = ord(self.rawfile.read(1)) 727 | try: 728 | code_name = { 729 | 0: "No reason specified", 730 | 1: "Key is superseded", 731 | 2: "Key material has been compromised", 732 | 3: "Key is retired and no longer used", 733 | 32: "User ID information is no longer valid", 734 | }[code_id] 735 | except KeyError: 736 | result['error'] = True 737 | result.setdefault("error_msg", []).append("Revocation code ({}) not recognized.".format(code_id)) 738 | subpacket['error'] = True 739 | subpacket.setdefault("error_msg", []).append("Revocation code ({}) not recognized.".format(code_id)) 740 | code_name = "Unknown" 741 | subpacket['code_id'] = code_id 742 | subpacket['code_name'] = code_name 743 | 744 | #revocation reason 745 | reason = self.rawfile.read(subpacket_len - 2) 746 | subpacket['reason'] = reason 747 | 748 | #Features (only first bit is defined) 749 | elif type_id == 30: 750 | modification_detection = ord(self.rawfile.read(1)) & 0x01 == 0x01 751 | subpacket['modification_detection'] = modification_detection 752 | 753 | #Signature Target 754 | elif type_id == 31: 755 | pubkey_algo = ord(self.rawfile.read(1)) 756 | hash_algo = ord(self.rawfile.read(1)) 757 | hash_result = self.rawfile.read(subpacket_len - 3).encode("hex") 758 | subpacket['pubkey_algo'] = pubkey_algo 759 | subpacket['hash_algo'] = hash_algo 760 | subpacket['hash'] = hash_result 761 | 762 | #Embedded Signature 763 | elif type_id == 32: 764 | sig_start = self.rawfile.tell() 765 | sig_len = subpacket_len - 1 766 | subpacket['signature'] = self.read_signature(sig_start, sig_len) 767 | 768 | result['subpackets'].append(subpacket) 769 | 770 | #go to the end of the subpacket section 771 | self.rawfile.seek(unhashed_subpacket_end) 772 | 773 | #hash check 774 | hash_check = self.rawfile.read(2).encode("hex") 775 | result['hash_check'] = hash_check 776 | 777 | #RSA signature 778 | if pubkey_algo_id in [1, 3]: 779 | 780 | #RSA signature value m**d mod n 781 | sig_len_bytes = self.rawfile.read(2) 782 | sig_len = int(sig_len_bytes.encode('hex'), 16) 783 | sig_numbytes = int(math.ceil(sig_len / 8.0)) 784 | if self.rawfile.tell() + sig_numbytes > body_start + body_len: 785 | result['error'] = True 786 | result.setdefault("error_msg", []).append("RSA signature overflows the overall length.") 787 | sig_numbytes = max(0, body_len - (self.rawfile.tell() - body_start)) 788 | sig_bytes = self.rawfile.read(sig_numbytes) 789 | if len(sig_bytes): 790 | sig_int = int(sig_bytes.encode('hex'), 16) 791 | if sig_int >> sig_len != 0: 792 | result['error'] = True 793 | result.setdefault("error_msg", []).append("RSA signature has non-zero leading bits.") 794 | sig_hex = "{0:0{1}x}".format(sig_int, sig_numbytes * 2) 795 | result['signature'] = sig_hex 796 | 797 | #DSA signature 798 | elif pubkey_algo_id in [17, 19, 22]: 799 | 800 | #DSA value r 801 | asn1_r = "" 802 | r_len_bytes = self.rawfile.read(2) 803 | r_len = int(r_len_bytes.encode('hex'), 16) 804 | r_numbytes = int(math.ceil(r_len / 8.0)) 805 | if self.rawfile.tell() + r_numbytes > body_start + body_len: 806 | result['error'] = True 807 | result.setdefault("error_msg", []).append("DSA signature r overflows the overall length.") 808 | r_numbytes = max(0, body_len - (self.rawfile.tell() - body_start)) 809 | r_bytes = self.rawfile.read(r_numbytes) 810 | if len(r_bytes): 811 | r_int = int(r_bytes.encode('hex'), 16) 812 | if r_int >> r_len != 0: 813 | result['error'] = True 814 | result.setdefault("error_msg", []).append("DSA signature r has non-zero leading bits.") 815 | r_hex = "{0:0{1}x}".format(r_int, r_numbytes * 2) 816 | result['signature_r'] = r_hex 817 | r_bigendian = r_hex #.decode("hex")[::-1].encode("hex") 818 | if ord(r_bigendian[0:2].decode("hex")) > 127: 819 | asn1_r = "02{0:0{1}x}00".format(len(r_bigendian) / 2 + 1, 2) + r_bigendian 820 | else: 821 | asn1_r = "02{0:0{1}x}".format(len(r_bigendian) / 2, 2) + r_bigendian 822 | 823 | #DSA value s 824 | asn1_s = "" 825 | s_len_bytes = self.rawfile.read(2) 826 | s_len = int(s_len_bytes.encode('hex'), 16) 827 | s_numbytes = int(math.ceil(s_len / 8.0)) 828 | if self.rawfile.tell() + s_numbytes > body_start + body_len: 829 | result['error'] = True 830 | result.setdefault("error_msg", []).append("DSA signature s overflows the overall length.") 831 | s_numbytes = max(0, body_len - (self.rawfile.tell() - body_start)) 832 | s_bytes = self.rawfile.read(s_numbytes) 833 | if len(s_bytes): 834 | s_int = int(s_bytes.encode('hex'), 16) 835 | if s_int >> s_len != 0: 836 | result['error'] = True 837 | result.setdefault("error_msg", []).append("DSA signature s has non-zero leading bits.") 838 | s_hex = "{0:0{1}x}".format(s_int, s_numbytes * 2) 839 | result['signature_s'] = s_hex 840 | s_bigendian = s_hex #.decode("hex")[::-1].encode("hex") 841 | if ord(s_bigendian[0:2].decode("hex")) > 127: 842 | asn1_s = "02{0:0{1}x}00".format(len(s_bigendian) / 2 + 1, 2) + s_bigendian 843 | else: 844 | asn1_s = "02{0:0{1}x}".format(len(s_bigendian) / 2, 2) + s_bigendian 845 | 846 | #asn.1 signature (for verifying signature via openssl) 847 | asn1_sig = "30{0:0{1}x}".format(len(asn1_r + asn1_s) / 2, 2) + asn1_r + asn1_s 848 | result['signature'] = asn1_sig 849 | 850 | #Reserved (formerly Elgamal Encrypt or Sign) 851 | elif pubkey_algo_id == 20: 852 | pass 853 | 854 | #Experimental 855 | elif pubkey_algo_id >= 100 and pubkey_algo_id <= 110: 856 | pass 857 | 858 | #reject all other types of signatures 859 | else: 860 | result['error'] = True 861 | result.setdefault("error_msg", []).append("Unsupported signature type ({}).".format(pubkey_algo_id)) 862 | return result 863 | 864 | #binary data document data 865 | if signature_type_name == "Signature of a binary document": 866 | #find the closest data packet 867 | i = len(self) - 1 868 | while i >= 0: 869 | if self[i]['tag_name'] == "Literal Data": 870 | msg_body = self[i]['data'] 871 | break 872 | i = i - 1 873 | 874 | #text data document data 875 | elif signature_type_name == "Signature of a canonical text document": 876 | #find the closest data packet 877 | i = len(self) - 1 878 | while i >= 0: 879 | if self[i]['tag_name'] == "Literal Data": 880 | msg_body = self[i]['data'] 881 | break 882 | i = i - 1 883 | #convert linebreaks to \r\n 884 | if "\r" not in msg_body: 885 | msg_body = msg_body.replace("\n", "\r\n") 886 | 887 | #direct key signature 888 | elif signature_type_name == "Signature directly on a key": 889 | #find the closest primary public key 890 | i = len(self) - 1 891 | while i >= 0: 892 | if self[i]['tag_name'] == "Public-Key": 893 | self.rawfile.seek(self[i]['body_start']) 894 | prefix = "\x99" 895 | len_octets = "{0:0{1}x}".format(self[i]['body_len'], 4).decode("hex") 896 | msg_body = prefix + len_octets + self.rawfile.read(self[i]['body_len']) 897 | break 898 | i = i - 1 899 | 900 | #user id/attribute document data 901 | elif signature_type_name in [ 902 | "Generic certification", 903 | "Persona certification", 904 | "Casual certification", 905 | "Positive certification", 906 | "Certification revocation", 907 | ]: 908 | #find the closest primary public key 909 | i = len(self) - 1 910 | while i >= 0: 911 | if self[i]['tag_name'] == "Public-Key": 912 | self.rawfile.seek(self[i]['body_start']) 913 | prefix = "\x99" 914 | len_octets = "{0:0{1}x}".format(self[i]['body_len'], 4).decode("hex") 915 | msg_body = prefix + len_octets + self.rawfile.read(self[i]['body_len']) 916 | break 917 | i = i - 1 918 | 919 | #find the closest user id or attribute 920 | i = len(self) - 1 921 | while i >= 0: 922 | 923 | #user id packet 924 | if self[i]['tag_name'] == "User ID": 925 | 926 | #no prefix for version 3 927 | if version == 3: 928 | prefix = "" 929 | len_octets = "" 930 | 931 | #version 4 prefix (0xB4) 932 | elif version == 4: 933 | prefix = "\xb4" 934 | len_octets = "{0:0{1}x}".format(self[i]['body_len'], 8).decode("hex") 935 | 936 | self.rawfile.seek(self[i]['body_start']) 937 | msg_body += prefix + len_octets + self.rawfile.read(self[i]['body_len']) 938 | break 939 | 940 | #user attribute packet 941 | elif self[i]['tag_name'] == "User Attribute": 942 | 943 | #no prefix for version 3 944 | if version == 3: 945 | prefix = "" 946 | len_octets = "" 947 | 948 | #version 4 prefix (0xD1) 949 | elif version == 4: 950 | prefix = "\xd1" 951 | len_octets = "{0:0{1}x}".format(self[i]['body_len'], 8).decode("hex") 952 | 953 | self.rawfile.seek(self[i]['body_start']) 954 | msg_body += prefix + len_octets + self.rawfile.read(self[i]['body_len']) 955 | break 956 | 957 | i = i - 1 958 | 959 | #subkey binding document data 960 | elif signature_type_name in ["Subkey Binding", "Primary Key Binding"]: 961 | 962 | #find the closest primary public key 963 | i = len(self) - 1 964 | while i >= 0: 965 | if self[i]['tag_name'] == "Public-Key": 966 | self.rawfile.seek(self[i]['body_start']) 967 | prefix = "\x99" 968 | len_octets = "{0:0{1}x}".format(self[i]['body_len'], 4).decode("hex") 969 | msg_body = prefix + len_octets + self.rawfile.read(self[i]['body_len']) 970 | break 971 | i = i - 1 972 | 973 | #find the closest subkey 974 | i = len(self) - 1 975 | while i >= 0: 976 | if self[i]['tag_name'] == "Public-Subkey": 977 | self.rawfile.seek(self[i]['body_start']) 978 | prefix = "\x99" 979 | len_octets = "{0:0{1}x}".format(self[i]['body_len'], 4).decode("hex") 980 | msg_body += prefix + len_octets + self.rawfile.read(self[i]['body_len']) 981 | break 982 | i = i - 1 983 | 984 | #primary key revoking document data 985 | elif signature_type_name == "Key revocation": 986 | 987 | #find the closest primary public key 988 | i = len(self) - 1 989 | while i >= 0: 990 | if self[i]['tag_name'] == "Public-Key": 991 | self.rawfile.seek(self[i]['body_start']) 992 | prefix = "\x99" 993 | len_octets = "{0:0{1}x}".format(self[i]['body_len'], 4).decode("hex") 994 | msg_body = prefix + len_octets + self.rawfile.read(self[i]['body_len']) 995 | break 996 | i = i - 1 997 | 998 | #subkey revoking document data 999 | elif signature_type_name == "Subkey revocation": 1000 | 1001 | #find the closest primary public key 1002 | i = len(self) - 1 1003 | while i >= 0: 1004 | if self[i]['tag_name'] == "Public-Subkey": 1005 | self.rawfile.seek(self[i]['body_start']) 1006 | prefix = "\x99" 1007 | len_octets = "{0:0{1}x}".format(self[i]['body_len'], 4).decode("hex") 1008 | msg_body = prefix + len_octets + self.rawfile.read(self[i]['body_len']) 1009 | break 1010 | i = i - 1 1011 | 1012 | #hash trailer 1013 | if version == 3: 1014 | trailer = "{0:0{1}x}".format(result['signature_type_id'], 2).decode("hex") 1015 | trailer += "{0:0{1}x}".format(result['creation_time'], 8).decode("hex") 1016 | elif version == 4: 1017 | hash_len = hashed_subpacket_end - body_start 1018 | self.rawfile.seek(body_start) 1019 | trailer = self.rawfile.read(hash_len) 1020 | trailer += "\x04\xff" 1021 | trailer += "{0:0{1}x}".format(hash_len, 8).decode("hex") 1022 | 1023 | #build data contents 1024 | data = msg_body + trailer 1025 | result['data'] = data.encode("hex") 1026 | 1027 | #hash result 1028 | if hash_algo_name == "MD5": 1029 | h = hashlib.md5() 1030 | elif hash_algo_name == "SHA1": 1031 | h = hashlib.sha1() 1032 | elif hash_algo_name == "RIPEMD160": 1033 | h = hashlib.new('ripemd160') 1034 | elif hash_algo_name == "SHA256": 1035 | h = hashlib.sha256() 1036 | elif hash_algo_name == "SHA384": 1037 | h = hashlib.sha384() 1038 | elif hash_algo_name == "SHA512": 1039 | h = hashlib.sha512() 1040 | elif hash_algo_name == "SHA224": 1041 | h = hashlib.sha224() 1042 | elif hash_algo_name == "Reserved": 1043 | result['error'] = True 1044 | result.setdefault("error_msg", []).append("Digest algorithm ({}) can't be checked.".format(result['hash_algo_id'])) 1045 | return result 1046 | else: 1047 | result['error'] = True 1048 | result.setdefault("error_msg", []).append("Unknown digest algorithm ({}).".format(result['hash_algo_id'])) 1049 | return result 1050 | h.update(data) 1051 | result['hash'] = h.hexdigest() 1052 | 1053 | #validate hash_check 1054 | if not result['hash'].startswith(hash_check): 1055 | result['error'] = True 1056 | result.setdefault("error_msg", []).append("Digest ({}) doesn't start with '{}'.".format(result['hash'], hash_check)) 1057 | return result 1058 | 1059 | return result 1060 | 1061 | def generate_signature(self, p): 1062 | 1063 | #version 1064 | bytes = "{0:0{1}x}".format(p['version'], 2).decode("hex") 1065 | 1066 | #signature body length (version 3 only) 1067 | if p['version'] == 3: 1068 | bytes += "{0:0{1}x}".format(5, 2).decode("hex") 1069 | 1070 | #signature type 1071 | bytes += "{0:0{1}x}".format(p['signature_type_id'], 2).decode("hex") 1072 | 1073 | #creation time (version 3 only) 1074 | if p['version'] == 3: 1075 | bytes += "{0:0{1}x}".format(p['creation_time'], 4).decode("hex") 1076 | 1077 | #signer key_id (version 3 only) 1078 | if p['version'] == 3: 1079 | bytes += p['key_id'].decode("hex") 1080 | 1081 | #public key algorithm 1082 | bytes += "{0:0{1}x}".format(p['pubkey_algo_id'], 2).decode("hex") 1083 | 1084 | #hash algorithm 1085 | bytes += "{0:0{1}x}".format(p['hash_algo_id'], 2).decode("hex") 1086 | 1087 | #subpackts (version 4 only) 1088 | if p['version'] == 4: 1089 | 1090 | #generate bytes for each subpacket 1091 | hashed_subpackets = "" 1092 | unhashed_subpackets = "" 1093 | for sp in p['subpackets']: 1094 | 1095 | #Signature Creation Time 1096 | if sp['type_id'] == 2: 1097 | sp_bytes = "{0:0{1}x}".format(sp['creation_time'], 8).decode("hex") 1098 | 1099 | #Signature Expiration Time 1100 | elif sp['type_id'] == 3: 1101 | sp_bytes = "{0:0{1}x}".format(sp['expiration_time'], 8).decode("hex") 1102 | 1103 | #Exportable Certification 1104 | elif sp['type_id'] == 4: 1105 | sp_bytes = "\x01" if sp['exportable'] else "\x00" 1106 | 1107 | #Trust Signature 1108 | elif sp['type_id'] == 5: 1109 | sp_bytes = "{0:0{1}x}".format(sp['level'], 2).decode("hex") 1110 | sp_bytes += "{0:0{1}x}".format(sp['amount'], 2).decode("hex") 1111 | 1112 | #Regular Expression 1113 | elif sp['type_id'] == 6: 1114 | sp_bytes = sp['regex'] 1115 | 1116 | #Revocable 1117 | elif sp['type_id'] == 7: 1118 | sp_bytes = "\x01" if sp['revocable'] else "\x00" 1119 | 1120 | #Key Expiration Time 1121 | elif sp['type_id'] == 9: 1122 | sp_bytes = "{0:0{1}x}".format(sp['expiration_time'], 8).decode("hex") 1123 | 1124 | #Preferred Symmetric Algorithms 1125 | elif sp['type_id'] == 11: 1126 | sp_bytes = "" 1127 | for a in sp['algos']: 1128 | sp_bytes += "{0:0{1}x}".format(a, 2).decode("hex") 1129 | 1130 | #Revocation Key 1131 | elif sp['type_id'] == 12: 1132 | sp_bytes = "\xc0" if sp['sensitive'] else "\x80" 1133 | sp_bytes += "{0:0{1}x}".format(sp['pubkey_algo'], 2).decode("hex") 1134 | sp_bytes += "{0:0{1}x}".format(sp['fingerprint'], 40).decode("hex") 1135 | 1136 | #Issuer 1137 | elif sp['type_id'] == 16: 1138 | sp_bytes = sp['key_id'].decode("hex") 1139 | 1140 | #Notation Data 1141 | elif sp['type_id'] == 20: 1142 | sp_bytes = "\x80\x00\x00\x00" if sp['human_readable'] else "\x00\x00\x00\x00" 1143 | sp_bytes += "{0:0{1}x}".format(len(sp['name']), 4).decode("hex") 1144 | sp_bytes += "{0:0{1}x}".format(len(sp['value']), 4).decode("hex") 1145 | sp_bytes += sp['name'] 1146 | sp_bytes += sp['value'] 1147 | 1148 | #Preferred Hash Algorithms 1149 | elif sp['type_id'] == 21: 1150 | sp_bytes = "" 1151 | for a in sp['algos']: 1152 | sp_bytes += "{0:0{1}x}".format(a, 2).decode("hex") 1153 | 1154 | #Preferred Compression Algorithms 1155 | elif sp['type_id'] == 22: 1156 | sp_bytes = "" 1157 | for a in sp['algos']: 1158 | sp_bytes += "{0:0{1}x}".format(a, 2).decode("hex") 1159 | 1160 | #Key Server Preferences (only first bit is defined) 1161 | elif sp['type_id'] == 23: 1162 | sp_bytes = "\x80" if sp['no_modify'] else "\x00" 1163 | 1164 | #Preferred Key Server 1165 | elif sp['type_id'] == 24: 1166 | sp_bytes = sp['keyserver'] 1167 | 1168 | #Primary User ID 1169 | elif sp['type_id'] == 25: 1170 | sp_bytes = "\x01" if sp['is_primary'] else "\x00" 1171 | 1172 | #Policy URI 1173 | elif sp['type_id'] == 26: 1174 | sp_bytes = sp['uri'] 1175 | 1176 | #Key Flags (only first octet has defined flags) 1177 | elif sp['type_id'] == 27: 1178 | flags = 0 1179 | flags |= 0x01 if sp['can_certify'] else 0x00 1180 | flags |= 0x02 if sp['can_sign'] else 0x00 1181 | flags |= 0x04 if sp['can_encrypt_communication'] else 0x00 1182 | flags |= 0x08 if sp['can_encrypt_storage'] else 0x00 1183 | flags |= 0x20 if sp['can_authenticate'] else 0x00 1184 | flags |= 0x10 if sp['private_might_be_split'] else 0x00 1185 | flags |= 0x80 if sp['private_might_be_shared'] else 0x00 1186 | sp_bytes = "{0:0{1}x}".format(flags, 2).decode("hex") 1187 | 1188 | #Signer's User ID 1189 | elif sp['type_id'] == 28: 1190 | sp_bytes = sp['keyserver'] 1191 | 1192 | #Reason for Revocation 1193 | elif sp['type_id'] == 29: 1194 | sp_bytes = "{0:0{1}x}".format(sp['code_id'], 2).decode("hex") 1195 | sp_bytes += sp['reason'] 1196 | 1197 | #Features (only first bit is defined) 1198 | elif sp['type_id'] == 30: 1199 | sp_bytes = "\x01" if sp['modification_detection'] else "\x00" 1200 | 1201 | #Signature Target 1202 | elif sp['type_id'] == 31: 1203 | sp_bytes = "{0:0{1}x}".format(sp['pubkey_algo'], 2).decode("hex") 1204 | sp_bytes += "{0:0{1}x}".format(sp['hash_algo'], 2).decode("hex") 1205 | sp_bytes += sp['hash'].decode("hex") 1206 | 1207 | #Embedded Signature 1208 | elif sp['type_id'] == 32: 1209 | sp_bytes = self.generate_signature(sp['signature']) 1210 | 1211 | #calculate subpacket length 1212 | sp_header = "" 1213 | sp_len = len(sp_bytes) + 1 1214 | 1215 | #one byte length 1216 | if sp_len < 192: 1217 | sp_header += "{0:0{1}x}".format(sp_len, 2).decode("hex") 1218 | 1219 | #two bytes length 1220 | elif sp_len >= 192 and sp_len <= 16575: 1221 | octets = (sp_len - 192) | 0xC000 1222 | sp_header += "{0:0{1}x}".format(octets, 4).decode("hex") 1223 | 1224 | #five bytes length 1225 | elif sp_len > 16575: 1226 | sp_header += "ff{0:0{1}x}".format(sp_len, 8).decode("hex") 1227 | 1228 | #add type and critical flag 1229 | sp_type = sp['type_id'] 1230 | if sp['critical']: 1231 | sp_type |= 0x80 1232 | sp_header += "{0:0{1}x}".format(sp_type, 2).decode("hex") 1233 | 1234 | #add to hashed or unhashed subpackets 1235 | if sp['hashed']: 1236 | hashed_subpackets += sp_header + sp_bytes 1237 | else: 1238 | unhashed_subpackets += sp_header + sp_bytes 1239 | 1240 | #calculate lengths 1241 | hashed_len = "{0:0{1}x}".format(len(hashed_subpackets), 4).decode("hex") 1242 | unhashed_len = "{0:0{1}x}".format(len(unhashed_subpackets), 4).decode("hex") 1243 | 1244 | bytes += hashed_len + hashed_subpackets 1245 | bytes += unhashed_len + unhashed_subpackets 1246 | 1247 | #hash check 1248 | bytes += p['hash_check'].decode("hex") 1249 | 1250 | #RSA signature 1251 | if p['pubkey_algo_id'] in [1, 3]: 1252 | 1253 | #RSA signature value m**d mod n 1254 | sig_int = int(p['signature'], 16) 1255 | bytes += "{0:0{1}x}".format(sig_int.bit_length(), 4).decode("hex") 1256 | bytes += p['signature'].decode("hex") 1257 | 1258 | #DSA signature 1259 | elif p['pubkey_algo_id'] == 17: 1260 | 1261 | #DSA value r 1262 | r_int = int(p['signature_r'], 16) 1263 | bytes += "{0:0{1}x}".format(r_int.bit_length(), 4).decode("hex") 1264 | bytes += p['signature_r'].decode("hex") 1265 | 1266 | #DSA value s 1267 | s_int = int(p['signature_s'], 16) 1268 | bytes += "{0:0{1}x}".format(s_int.bit_length(), 4).decode("hex") 1269 | bytes += p['signature_s'].decode("hex") 1270 | 1271 | return bytes 1272 | 1273 | 1274 | def read_onepasssig(self, body_start, body_len): 1275 | """ 1276 | Specifications: 1277 | https://tools.ietf.org/html/rfc4880#section-5.4 1278 | 1279 | Signature Types: 1280 | ID Signature type 1281 | -- --------- 1282 | 0 (0x00) - Signature of a binary document 1283 | 1 (0x01) - Signature of a canonical text document 1284 | 2 (0x02) - Standalone signature 1285 | 16 (0x10) - Generic certification of a User ID and Public-Key packet 1286 | 17 (0x11) - Persona certification of a User ID and Public-Key packet 1287 | 18 (0x12) - Casual certification of a User ID and Public-Key packet 1288 | 19 (0x13) - Positive certification of a User ID and Public-Key packet 1289 | 24 (0x18) - Subkey Binding Signature 1290 | 25 (0x19) - Primary Key Binding Signature 1291 | 31 (0x1F) - Signature directly on a key 1292 | 32 (0x20) - Key revocation signature 1293 | 40 (0x28) - Subkey revocation signature 1294 | 48 (0x30) - Certification revocation signature 1295 | 64 (0x40) - Timestamp signature 1296 | 80 (0x50) - Third-Party Confirmation signature 1297 | 1298 | Public Key Algorithms: 1299 | ID Algorithm 1300 | -- --------- 1301 | 1 - RSA (Encrypt or Sign) 1302 | 2 - RSA Encrypt-Only 1303 | 3 - RSA Sign-Only 1304 | 16 - Elgamal (Encrypt-Only) 1305 | 17 - DSA (Digital Signature Algorithm) 1306 | 18 - ECDH public key algorithm 1307 | 19 - ECDSA public key algorithm 1308 | 20 - Reserved (formerly Elgamal Encrypt or Sign) 1309 | 21 - Reserved for Diffie-Hellman (X9.42, as defined for IETF-S/MIME) 1310 | 100 to 110 - Private/Experimental algorithm 1311 | 1312 | Hash Algorithms: 1313 | ID Algorithm 1314 | -- --------- 1315 | 1 - MD5 1316 | 2 - SHA1 1317 | 3 - RIPEMD160 1318 | 4 - Reserved 1319 | 5 - Reserved 1320 | 6 - Reserved 1321 | 7 - Reserved 1322 | 8 - SHA256 1323 | 9 - SHA384 1324 | 10 - SHA512 1325 | 11 - SHA224 1326 | 100 to 110 - Private/Experimental algorithm 1327 | 1328 | Return Format: 1329 | { 1330 | #standard packet values 1331 | "tag_id": 4, 1332 | "tag_name": "One-Pass Signature", 1333 | "body_start": 0, 1334 | "body_len": 123, 1335 | 1336 | #errors (if any) 1337 | "error": True, 1338 | "error_msg": ["Error msg 1", "Error msg 2"], 1339 | 1340 | #one-pass signature packet values 1341 | "version": 3, 1342 | "signature_type_id": 16, 1343 | "signature_type_name": "Generic certification of a User ID and Public-Key packet", 1344 | "hash_algo_id": 8, 1345 | "hash_algo_name": "SHA256", 1346 | "pubkey_algo_id": 1, 1347 | "pubkey_algo_name": "RSA (Encrypt or Sign)", 1348 | "key_id": "deadbeefdeadbeef", 1349 | "nested": True or False, 1350 | } 1351 | 1352 | """ 1353 | result = { 1354 | "tag_id": 4, 1355 | "tag_name": "One-Pass Signature", 1356 | "body_start": body_start, 1357 | "body_len": body_len, 1358 | } 1359 | 1360 | #version 1361 | self.rawfile.seek(body_start) 1362 | version = ord(self.rawfile.read(1)) 1363 | if version != 3: 1364 | result['error'] = True 1365 | result.setdefault("error_msg", []).append("One-Pass Signature version is invalid ({}).".format(version)) 1366 | return result 1367 | result['version'] = version 1368 | 1369 | #signature type 1370 | signature_type_id = ord(self.rawfile.read(1)) 1371 | try: 1372 | signature_type_name = { 1373 | 0: "Signature of a binary document", 1374 | 1: "Signature of a canonical text document", 1375 | 2: "Standalone signature", 1376 | 16: "Generic certification", 1377 | 17: "Persona certification", 1378 | 18: "Casual certification", 1379 | 19: "Positive certification", 1380 | 24: "Subkey Binding", 1381 | 25: "Primary Key Binding", 1382 | 31: "Signature directly on a key", 1383 | 32: "Key revocation", 1384 | 40: "Subkey revocation", 1385 | 48: "Certification revocation", 1386 | 64: "Timestamp", 1387 | 80: "Third-Party Confirmation", 1388 | }[signature_type_id] 1389 | except KeyError: 1390 | signature_type_name = "Unknown" 1391 | result['error'] = True 1392 | result.setdefault("error_msg", []).append("Signature type ({}) not recognized.".format(signature_type_id)) 1393 | result['signature_type_id'] = signature_type_id 1394 | result['signature_type_name'] = signature_type_name 1395 | 1396 | #hash algorithm 1397 | hash_algo_id = ord(self.rawfile.read(1)) 1398 | try: 1399 | hash_algo_name = { 1400 | 1: "MD5", 1401 | 2: "SHA1", 1402 | 3: "RIPEMD160", 1403 | 4: "Reserved", 1404 | 5: "Reserved", 1405 | 6: "Reserved", 1406 | 7: "Reserved", 1407 | 8: "SHA256", 1408 | 9: "SHA384", 1409 | 10: "SHA512", 1410 | 11: "SHA224", 1411 | }[hash_algo_id] 1412 | except KeyError: 1413 | hash_algo_name = "Unknown" 1414 | result['error'] = True 1415 | result.setdefault("error_msg", []).append("Hash algorithm ({}) not recognized.".format(hash_algo_id)) 1416 | result['hash_algo_id'] = hash_algo_id 1417 | result['hash_algo_name'] = hash_algo_name 1418 | 1419 | #public key algorithm 1420 | pubkey_algo_id = ord(self.rawfile.read(1)) 1421 | try: 1422 | pubkey_algo_name = { 1423 | 1: "RSA (Encrypt or Sign)", 1424 | 2: "RSA Encrypt-Only", 1425 | 3: "RSA Sign-Only", 1426 | 16: "Elgamal (Encrypt-Only)", 1427 | 17: "DSA (Digital Signature Algorithm)", 1428 | 18: "ECDH public key algorithm", 1429 | 19: "ECDSA public key algorithm", 1430 | 20: "Reserved (formerly Elgamal Encrypt or Sign)", 1431 | 21: "Reserved for Diffie-Hellman (X9.42, as defined for IETF-S/MIME)", 1432 | 22: "EdDSA public key algorithm", 1433 | 100: "Private or experimental", 1434 | 101: "Private or experimental", 1435 | 102: "Private or experimental", 1436 | 103: "Private or experimental", 1437 | 104: "Private or experimental", 1438 | 105: "Private or experimental", 1439 | 106: "Private or experimental", 1440 | 107: "Private or experimental", 1441 | 108: "Private or experimental", 1442 | 109: "Private or experimental", 1443 | 110: "Private or experimental", 1444 | }[pubkey_algo_id] 1445 | except KeyError: 1446 | pubkey_algo_name = "Unknown" 1447 | result['error'] = True 1448 | result.setdefault("error_msg", []).append("Public-Key algorithm ({}) not recognized.".format(pubkey_algo_id)) 1449 | result['pubkey_algo_id'] = pubkey_algo_id 1450 | result['pubkey_algo_name'] = pubkey_algo_name 1451 | 1452 | #signer key_id 1453 | key_id = self.rawfile.read(8).encode("hex") 1454 | result['key_id'] = key_id 1455 | 1456 | #is nested (0 means nested) 1457 | nested_raw = ord(self.rawfile.read(1)) 1458 | result['nested'] = nested_raw == 0 1459 | 1460 | return result 1461 | 1462 | 1463 | def read_pubkey(self, body_start, body_len): 1464 | """ 1465 | Specifications: 1466 | https://tools.ietf.org/html/rfc4880#section-5.5.1.1 1467 | https://tools.ietf.org/html/rfc4880#section-5.5.2 1468 | https://tools.ietf.org/html/rfc4880#section-9.1 1469 | 1470 | Public Key Algorithms: 1471 | ID Algorithm 1472 | -- --------- 1473 | 1 - RSA (Encrypt or Sign) 1474 | 2 - RSA Encrypt-Only 1475 | 3 - RSA Sign-Only 1476 | 16 - Elgamal (Encrypt-Only) 1477 | 17 - DSA (Digital Signature Algorithm) 1478 | 18 - ECDH public key algorithm 1479 | 19 - ECDSA public key algorithm 1480 | 20 - Reserved (formerly Elgamal Encrypt or Sign) 1481 | 21 - Reserved for Diffie-Hellman (X9.42, as defined for IETF-S/MIME) 1482 | 100 to 110 - Private/Experimental algorithm 1483 | 1484 | Return Format: 1485 | { 1486 | #standard packet values 1487 | "tag_id": 6, 1488 | "tag_name": "Public-Key", 1489 | "body_start": 0, 1490 | "body_len": 123, 1491 | 1492 | #errors (if any) 1493 | "error": True, 1494 | "error_msg": ["Error msg 1", "Error msg 2"], 1495 | 1496 | #public key packet values 1497 | "key_id": "deadbeefdeadbeef", 1498 | "fingerprint": "deadbeefdeadbeefdeadbeefdeadbeef", 1499 | "version": 3 or 4, 1500 | "algo_id": 1, 1501 | "algo_name": "RSA (Encrypt or Sign)", 1502 | "creation_time": 1234567890, 1503 | "valid_days": 30, #version 3 only 1504 | 1505 | #public key for openssl verification 1506 | #(e.g. openssl dgst -sha1 -verify -signature ) 1507 | "pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----", 1508 | 1509 | #RSA specific (algo_ids 1, 2, 3) 1510 | "n": "deadbeef", #RSA public modulus n 1511 | "e": "deadbeef", #RSA public encryption exponent e 1512 | 1513 | #Elgamal specific (algo_ids 16, 20) 1514 | "p": "deadbeef", #Elgamal prime p 1515 | "g": "deadbeef", #Elgamal group generator g 1516 | 1517 | #DSA specific (algo_id 17) 1518 | "p": "deadbeef", #DSA prime p 1519 | "q": "deadbeef", #DSA group order q (q is a prime divisor of p-1) 1520 | "g": "deadbeef", #DSA group generator g 1521 | "y": "deadbeef", #DSA public-key value y (= g**x mod p where x is secret) 1522 | 1523 | #ECDH specific (algo_id 18) 1524 | #TODO 1525 | 1526 | #ECDSA specific (algo_id 19) 1527 | "curve": "P-256", #(P-256|P-384|P-521) 1528 | "x": "deadbeef", 1529 | "y": "deadbeef", 1530 | } 1531 | """ 1532 | result = { 1533 | "tag_id": 6, 1534 | "tag_name": "Public-Key", 1535 | "body_start": body_start, 1536 | "body_len": body_len, 1537 | } 1538 | 1539 | #version 1540 | self.rawfile.seek(body_start) 1541 | version = ord(self.rawfile.read(1)) 1542 | if version not in [3, 4]: 1543 | result['error'] = True 1544 | result.setdefault("error_msg", []).append("Public Key version is invalid ({}).".format(version)) 1545 | return result 1546 | result['version'] = version 1547 | 1548 | #creation date 1549 | creation_bytes = self.rawfile.read(4) 1550 | creation_time = int(creation_bytes.encode('hex'), 16) 1551 | result['creation_time'] = creation_time 1552 | 1553 | #expire days (version 3 only) 1554 | if version == 3: 1555 | valid_bytes = self.rawfile.read(2) 1556 | valid_days = int(valid_bytes.encode('hex'), 16) 1557 | result['valid_days'] = valid_days 1558 | 1559 | #algorithm 1560 | algo_id = ord(self.rawfile.read(1)) 1561 | try: 1562 | algo_name = { 1563 | 1: "RSA (Encrypt or Sign)", 1564 | 2: "RSA Encrypt-Only", 1565 | 3: "RSA Sign-Only", 1566 | 16: "Elgamal (Encrypt-Only)", 1567 | 17: "DSA (Digital Signature Algorithm)", 1568 | 18: "ECDH public key algorithm", 1569 | 19: "ECDSA public key algorithm", 1570 | 20: "Reserved (formerly Elgamal Encrypt or Sign)", 1571 | 21: "Reserved for Diffie-Hellman (X9.42, as defined for IETF-S/MIME)", 1572 | 22: "EdDSA public key algorithm", 1573 | 100: "Private or experimental", 1574 | 101: "Private or experimental", 1575 | 102: "Private or experimental", 1576 | 103: "Private or experimental", 1577 | 104: "Private or experimental", 1578 | 105: "Private or experimental", 1579 | 106: "Private or experimental", 1580 | 107: "Private or experimental", 1581 | 108: "Private or experimental", 1582 | 109: "Private or experimental", 1583 | 110: "Private or experimental", 1584 | }[algo_id] 1585 | except KeyError: 1586 | result['error'] = True 1587 | result.setdefault("error_msg", []).append("Public-Key algorithm ({}) not recognized.".format(algo_id)) 1588 | result['algo_id'] = algo_id 1589 | return result 1590 | result['algo_id'] = algo_id 1591 | result['algo_name'] = algo_name 1592 | 1593 | #RSA 1594 | if algo_id in [1, 2, 3]: 1595 | pem = "" 1596 | 1597 | #modulus 1598 | n_len_bytes = self.rawfile.read(2) 1599 | n_len = int(n_len_bytes.encode('hex'), 16) 1600 | n_numbytes = int(math.ceil(n_len / 8.0)) 1601 | if self.rawfile.tell() + n_numbytes > body_start + body_len: 1602 | result['error'] = True 1603 | result.setdefault("error_msg", []).append("RSA modulus overflows the overall length.") 1604 | n_numbytes = max(0, body_len - (self.rawfile.tell() - body_start)) 1605 | n_bytes = self.rawfile.read(n_numbytes) 1606 | if len(n_bytes): 1607 | n_int = int(n_bytes.encode('hex'), 16) 1608 | if n_int >> n_len != 0: 1609 | result['error'] = True 1610 | result.setdefault("error_msg", []).append("RSA modulus has non-zero leading bits.") 1611 | n_hex = "{0:0{1}x}".format(n_int, n_numbytes * 2) 1612 | result['n'] = n_hex 1613 | pem += "0282{0:0{1}x}00".format(n_numbytes + 1, 4) + n_hex 1614 | 1615 | #exponent 1616 | e_len_bytes = self.rawfile.read(2) 1617 | e_len = int(e_len_bytes.encode('hex'), 16) 1618 | e_numbytes = int(math.ceil(e_len / 8.0)) 1619 | if self.rawfile.tell() + e_numbytes > body_start + body_len: 1620 | result['error'] = True 1621 | result.setdefault("error_msg", []).append("RSA exponent overflows the overall length.") 1622 | e_numbytes = max(0, body_len - (self.rawfile.tell() - body_start)) 1623 | e_bytes = self.rawfile.read(e_numbytes) 1624 | if len(e_bytes): 1625 | e_int = int(e_bytes.encode('hex'), 16) 1626 | if e_int >> e_len != 0: 1627 | result['error'] = True 1628 | result.setdefault("error_msg", []).append("RSA exponent has non-zero leading bits.") 1629 | e_hex = "{0:0{1}x}".format(e_int, e_numbytes * 2) 1630 | result['e'] = e_hex 1631 | pem += "0282{0:0{1}x}".format(e_numbytes, 4) + e_hex 1632 | 1633 | #pem format 1634 | pem_seq = "3082{0:0{1}x}".format(len(pem) / 2, 4) + pem 1635 | pem_bitseq = "0382{0:0{1}x}00".format(len(pem_seq) / 2 + 1, 4) + pem_seq 1636 | pem_rsa = "300d06092a864886f70d0101010500" + pem_bitseq 1637 | pem_full = "3082{0:0{1}x}".format(len(pem_rsa) / 2, 4) + pem_rsa 1638 | pem_bytes = pem_full.decode("hex") 1639 | 1640 | #Elgamal 1641 | elif algo_id in [16, 20]: 1642 | 1643 | #prime p 1644 | p_len_bytes = self.rawfile.read(2) 1645 | p_len = int(p_len_bytes.encode('hex'), 16) 1646 | p_numbytes = int(math.ceil(p_len / 8.0)) 1647 | if self.rawfile.tell() + p_numbytes > body_start + body_len: 1648 | result['error'] = True 1649 | result.setdefault("error_msg", []).append("Elgamal prime p overflows the overall length.") 1650 | p_numbytes = max(0, body_len - (self.rawfile.tell() - body_start)) 1651 | p_bytes = self.rawfile.read(p_numbytes) 1652 | if len(p_bytes): 1653 | p_int = int(p_bytes.encode('hex'), 16) 1654 | if p_int >> p_len != 0: 1655 | result['error'] = True 1656 | result.setdefault("error_msg", []).append("Elgamal prime p has non-zero leading bits.") 1657 | p_hex = "{0:0{1}x}".format(p_int, p_numbytes * 2) 1658 | result['p'] = p_hex 1659 | 1660 | #generator g 1661 | g_len_bytes = self.rawfile.read(2) 1662 | g_len = int(g_len_bytes.encode('hex'), 16) 1663 | g_numbytes = int(math.ceil(g_len / 8.0)) 1664 | if self.rawfile.tell() + g_numbytes > body_start + body_len: 1665 | result['error'] = True 1666 | result.setdefault("error_msg", []).append("Elgamal generator g overflows the overall length.") 1667 | g_numbytes = max(0, body_len - (self.rawfile.tell() - body_start)) 1668 | g_bytes = self.rawfile.read(g_numbytes) 1669 | if len(g_bytes): 1670 | g_int = int(g_bytes.encode('hex'), 16) 1671 | if g_int >> g_len != 0: 1672 | result['error'] = True 1673 | result.setdefault("error_msg", []).append("Elgamal generator g has non-zero leading bits.") 1674 | g_hex = "{0:0{1}x}".format(g_int, g_numbytes * 2) 1675 | result['g'] = g_hex 1676 | 1677 | #public-key value y 1678 | y_len_bytes = self.rawfile.read(2) 1679 | y_len = int(y_len_bytes.encode('hex'), 16) 1680 | y_numbytes = int(math.ceil(y_len / 8.0)) 1681 | if self.rawfile.tell() + y_numbytes > body_start + body_len: 1682 | result['error'] = True 1683 | result.setdefault("error_msg", []).append("Elgamal public-key value y overflows the overall length.") 1684 | y_numbytes = max(0, body_len - (self.rawfile.tell() - body_start)) 1685 | y_bytes = self.rawfile.read(y_numbytes) 1686 | if len(y_bytes): 1687 | y_int = int(y_bytes.encode('hex'), 16) 1688 | if y_int >> y_len != 0: 1689 | result['error'] = True 1690 | result.setdefault("error_msg", []).append("Elgamal public-key value y has non-zero leading bits.") 1691 | y_hex = "{0:0{1}x}".format(y_int, y_numbytes * 2) 1692 | result['y'] = y_hex 1693 | 1694 | #TODO: Make real pem bytes 1695 | pem_bytes = "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" 1696 | 1697 | #DSA 1698 | elif algo_id == 17: 1699 | pem_params = "" 1700 | 1701 | #prime p 1702 | p_len_bytes = self.rawfile.read(2) 1703 | p_len = int(p_len_bytes.encode('hex'), 16) 1704 | p_numbytes = int(math.ceil(p_len / 8.0)) 1705 | if self.rawfile.tell() + p_numbytes > body_start + body_len: 1706 | result['error'] = True 1707 | result.setdefault("error_msg", []).append("DSA prime p overflows the overall length.") 1708 | p_numbytes = max(0, body_len - (self.rawfile.tell() - body_start)) 1709 | p_bytes = self.rawfile.read(p_numbytes) 1710 | if len(p_bytes): 1711 | p_int = int(p_bytes.encode('hex'), 16) 1712 | if p_int >> p_len != 0: 1713 | result['error'] = True 1714 | result.setdefault("error_msg", []).append("DSA prime p has non-zero leading bits.") 1715 | p_hex = "{0:0{1}x}".format(p_int, p_numbytes * 2) 1716 | result['p'] = p_hex 1717 | pem_params += "0282{0:0{1}x}00".format(p_numbytes + 1, 4) + p_hex 1718 | 1719 | #group order q 1720 | q_len_bytes = self.rawfile.read(2) 1721 | q_len = int(q_len_bytes.encode('hex'), 16) 1722 | q_numbytes = int(math.ceil(q_len / 8.0)) 1723 | if self.rawfile.tell() + q_numbytes > body_start + body_len: 1724 | result['error'] = True 1725 | result.setdefault("error_msg", []).append("DSA group order q overflows the overall length.") 1726 | q_numbytes = max(0, body_len - (self.rawfile.tell() - body_start)) 1727 | q_bytes = self.rawfile.read(q_numbytes) 1728 | if len(q_bytes): 1729 | q_int = int(q_bytes.encode('hex'), 16) 1730 | if q_int >> q_len != 0: 1731 | result['error'] = True 1732 | result.setdefault("error_msg", []).append("DSA group order q has non-zero leading bits.") 1733 | q_hex = "{0:0{1}x}".format(q_int, q_numbytes * 2) 1734 | result['q'] = q_hex 1735 | pem_params += "0282{0:0{1}x}00".format(q_numbytes + 1, 4) + q_hex 1736 | 1737 | #generator g 1738 | g_len_bytes = self.rawfile.read(2) 1739 | g_len = int(g_len_bytes.encode('hex'), 16) 1740 | g_numbytes = int(math.ceil(g_len / 8.0)) 1741 | if self.rawfile.tell() + g_numbytes > body_start + body_len: 1742 | result['error'] = True 1743 | result.setdefault("error_msg", []).append("DSA generator g overflows the overall length.") 1744 | g_numbytes = max(0, body_len - (self.rawfile.tell() - body_start)) 1745 | g_bytes = self.rawfile.read(g_numbytes) 1746 | if len(g_bytes): 1747 | g_int = int(g_bytes.encode('hex'), 16) 1748 | if g_int >> g_len != 0: 1749 | result['error'] = True 1750 | result.setdefault("error_msg", []).append("DSA generator g has non-zero leading bits.") 1751 | g_hex = "{0:0{1}x}".format(g_int, g_numbytes * 2) 1752 | result['g'] = g_hex 1753 | pem_params += "0282{0:0{1}x}00".format(g_numbytes + 1, 4) + g_hex 1754 | 1755 | #public-key value y 1756 | pem_y = "" 1757 | y_len_bytes = self.rawfile.read(2) 1758 | y_len = int(y_len_bytes.encode('hex'), 16) 1759 | y_numbytes = int(math.ceil(y_len / 8.0)) 1760 | if self.rawfile.tell() + y_numbytes > body_start + body_len: 1761 | result['error'] = True 1762 | result.setdefault("error_msg", []).append("DSA public-key value y overflows the overall length.") 1763 | y_numbytes = max(0, body_len - (self.rawfile.tell() - body_start)) 1764 | y_bytes = self.rawfile.read(y_numbytes) 1765 | if len(y_bytes): 1766 | y_int = int(y_bytes.encode('hex'), 16) 1767 | if y_int >> y_len != 0: 1768 | result['error'] = True 1769 | result.setdefault("error_msg", []).append("DSA public-key value y has non-zero leading bits.") 1770 | y_hex = "{0:0{1}x}".format(y_int, y_numbytes * 2) 1771 | result['y'] = y_hex 1772 | pem_y += "0282{0:0{1}x}00".format(y_numbytes + 1, 4) + y_hex 1773 | 1774 | #pem format 1775 | pem_paramseq = "3082{0:0{1}x}".format(len(pem_params) / 2, 4) + pem_params 1776 | pem_dsaseq = "3082{0:0{1}x}06072a8648ce380401".format((len(pem_paramseq) + 18) / 2, 4) + pem_paramseq 1777 | pem_pubbits = "0382{0:0{1}x}00".format(len(pem_y) / 2 + 1, 4) + pem_y 1778 | pem_full = "3082{0:0{1}x}".format(len(pem_dsaseq + pem_pubbits) / 2, 4) + pem_dsaseq + pem_pubbits 1779 | pem_bytes = pem_full.decode("hex") 1780 | 1781 | #ECDH 1782 | elif algo_id == 18: 1783 | 1784 | #curve oid 1785 | oid_len = ord(self.rawfile.read(1)) 1786 | if self.rawfile.tell() + oid_len > body_start + body_len: 1787 | result['error'] = True 1788 | result.setdefault("error_msg", []).append("ECDH OID length overflows the overall length.") 1789 | oid_len = max(0, body_len - (self.rawfile.tell() - body_start)) 1790 | oid = self.rawfile.read(oid_len) 1791 | try: 1792 | curve_name = { 1793 | "2a8648ce3d030107": "NIST curve P-256", 1794 | "2b81040022": "NIST curve P-384", 1795 | "2b81040023": "NIST curve P-521", 1796 | "2b8104000a": "secp256k1", 1797 | "2b2403030208010107": "brainpoolP256r1", 1798 | "2b240303020801010b": "brainpoolP384r1", 1799 | "2b240303020801010d": "brainpoolP512r1", 1800 | }[oid.encode("hex")] 1801 | except KeyError: 1802 | curve_name = "Unknown" 1803 | result['error'] = True 1804 | result.setdefault("error_msg", []).append("ECDH has unknown curve OID ('{}').".format(oid.encode("hex"))) 1805 | result['oid'] = oid 1806 | result['curve_name'] = curve_name 1807 | 1808 | #public key coords 1809 | coords_len_bytes = self.rawfile.read(2) 1810 | coords_len = int(coords_len_bytes.encode('hex'), 16) 1811 | coords_numbytes = int(math.ceil(coords_len / 8.0)) 1812 | if self.rawfile.tell() + coords_numbytes > body_start + body_len: 1813 | result['error'] = True 1814 | result.setdefault("error_msg", []).append("ECDH coords overflows the overall length.") 1815 | coords_numbytes = max(0, body_len - (self.rawfile.tell() - body_start)) 1816 | coords_bytes = self.rawfile.read(coords_numbytes) 1817 | if len(coords_bytes): 1818 | coords_int = int(coords_bytes.encode('hex'), 16) 1819 | if coords_int >> coords_len != 0: 1820 | result['error'] = True 1821 | result.setdefault("error_msg", []).append("ECDH coords have non-zero leading bits.") 1822 | coords_hex = "{0:0{1}x}".format(coords_int, coords_numbytes * 2) 1823 | 1824 | #uncompressed coordinates 1825 | coords_x = coords_hex[1:(len(coords_hex) - 1)/2] 1826 | coords_y = coords_hex[(len(coords_hex) - 1)/2:] 1827 | result['x'] = coords_x 1828 | result['y'] = coords_y 1829 | 1830 | #KDF parameters 1831 | kdf_len = ord(self.rawfile.read(1)) 1832 | kdf_version = ord(self.rawfile.read(1)) 1833 | if kdf_version == 1: 1834 | kdf_hash_id = ord(self.rawfile.read(1)) 1835 | kdf_algo_id = ord(self.rawfile.read(1)) 1836 | result['kdf_hash_id'] = kdf_hash_id 1837 | result['kdf_algo_id'] = kdf_algo_id 1838 | else: 1839 | result['error'] = True 1840 | result.setdefault("error_msg", []).append("ECDH version ({}) is not 1.".format(kdf_version)) 1841 | 1842 | #TODO: Make real pem bytes 1843 | pem_bytes = "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" 1844 | 1845 | #ECDSA 1846 | elif algo_id == 19: 1847 | 1848 | #curve oid 1849 | oid_len = ord(self.rawfile.read(1)) 1850 | if self.rawfile.tell() + oid_len > body_start + body_len: 1851 | result['error'] = True 1852 | result.setdefault("error_msg", []).append("ECDSA OID length overflows the overall length.") 1853 | oid_len = max(0, body_len - (self.rawfile.tell() - body_start)) 1854 | oid = self.rawfile.read(oid_len) 1855 | try: 1856 | curve_name = { 1857 | "2a8648ce3d030107": "NIST curve P-256", 1858 | "2b81040022": "NIST curve P-384", 1859 | "2b81040023": "NIST curve P-521", 1860 | "2b8104000a": "secp256k1", 1861 | "2b2403030208010107": "brainpoolP256r1", 1862 | "2b240303020801010b": "brainpoolP384r1", 1863 | "2b240303020801010d": "brainpoolP512r1", 1864 | }[oid.encode("hex")] 1865 | except KeyError: 1866 | curve_name = "Unknown" 1867 | result['error'] = True 1868 | result.setdefault("error_msg", []).append("ECDSA has unknown curve OID ('{}').".format(oid.encode("hex"))) 1869 | result['oid'] = oid 1870 | result['curve_name'] = curve_name 1871 | 1872 | #public key coords 1873 | coords_len_bytes = self.rawfile.read(2) 1874 | coords_len = int(coords_len_bytes.encode('hex'), 16) 1875 | coords_numbytes = int(math.ceil(coords_len / 8.0)) 1876 | if self.rawfile.tell() + coords_numbytes > body_start + body_len: 1877 | result['error'] = True 1878 | result.setdefault("error_msg", []).append("ECDSA coords overflows the overall length.") 1879 | coords_numbytes = max(0, body_len - (self.rawfile.tell() - body_start)) 1880 | coords_bytes = self.rawfile.read(coords_numbytes) 1881 | if len(coords_bytes): 1882 | coords_int = int(coords_bytes.encode('hex'), 16) 1883 | if coords_int >> coords_len != 0: 1884 | result['error'] = True 1885 | result.setdefault("error_msg", []).append("ECDSA coords have non-zero leading bits.") 1886 | coords_hex = "{0:0{1}x}".format(coords_int, coords_numbytes * 2) 1887 | 1888 | #uncompressed coordinates 1889 | coords_x = coords_hex[1:(len(coords_hex) - 1)/2] 1890 | coords_y = coords_hex[(len(coords_hex) - 1)/2:] 1891 | result['x'] = coords_x 1892 | result['y'] = coords_y 1893 | 1894 | #TODO: Make real pem bytes 1895 | pem_bytes = "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" 1896 | 1897 | #DH 1898 | elif algo_id == 21: 1899 | raise NotImplementedError("DH public key parsing is not implemented yet :(") 1900 | 1901 | #EdDSA 1902 | elif algo_id == 22: 1903 | 1904 | #curve oid 1905 | oid_len = ord(self.rawfile.read(1)) 1906 | if self.rawfile.tell() + oid_len > body_start + body_len: 1907 | result['error'] = True 1908 | result.setdefault("error_msg", []).append("ECDSA OID length overflows the overall length.") 1909 | oid_len = max(0, body_len - (self.rawfile.tell() - body_start)) 1910 | oid = self.rawfile.read(oid_len) 1911 | try: 1912 | curve_name = { 1913 | "2b06010401da470f01": "Ed25519", 1914 | }[oid.encode("hex")] 1915 | except KeyError: 1916 | curve_name = "Unknown" 1917 | result['error'] = True 1918 | result.setdefault("error_msg", []).append("EdDSA has unknown curve OID ('{}').".format(oid.encode("hex"))) 1919 | result['oid'] = oid 1920 | result['curve_name'] = curve_name 1921 | 1922 | #public key coords 1923 | coords_len_bytes = self.rawfile.read(2) 1924 | coords_len = int(coords_len_bytes.encode('hex'), 16) 1925 | coords_numbytes = int(math.ceil(coords_len / 8.0)) 1926 | if self.rawfile.tell() + coords_numbytes > body_start + body_len: 1927 | result['error'] = True 1928 | result.setdefault("error_msg", []).append("EdDSA coords overflows the overall length.") 1929 | coords_numbytes = max(0, body_len - (self.rawfile.tell() - body_start)) 1930 | coords_bytes = self.rawfile.read(coords_numbytes) 1931 | if len(coords_bytes): 1932 | coords_int = int(coords_bytes.encode('hex'), 16) 1933 | if coords_int >> coords_len != 0: 1934 | result['error'] = True 1935 | result.setdefault("error_msg", []).append("EdDSA coords have non-zero leading bits.") 1936 | coords_hex = "{0:0{1}x}".format(coords_int, coords_numbytes * 2) 1937 | 1938 | #compressed format 1939 | if coords_hex.startswith("40"): 1940 | result['x'] = coords_hex[1:] 1941 | 1942 | #uncompressed format 1943 | elif coords_hex.startswith("04"): 1944 | result['x'] = coords_hex[1:(len(coords_hex) - 1)/2] 1945 | result['y'] = coords_hex[(len(coords_hex) - 1)/2:] 1946 | 1947 | #TODO: Make real pem bytes 1948 | pem_bytes = "\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff" 1949 | 1950 | #private/experimental 1951 | elif algo_id >= 100 and algo_id <= 110: 1952 | pem_bytes = None 1953 | 1954 | #reject all other algorithms 1955 | else: 1956 | pem_bytes = None 1957 | result['error'] = True 1958 | result.setdefault("error_msg", []).append("Public Key algorithm is invalid ({}).".format(algo_id)) 1959 | 1960 | #pem file 1961 | if pem_bytes: 1962 | p = base64.b64encode(pem_bytes) 1963 | pem_b64 = "\n".join([p[i*64:(i+1)*64] for i in xrange(0, int(math.ceil(len(p)/64))+1)]) 1964 | pem_str = "-----BEGIN PUBLIC KEY-----\n{}\n-----END PUBLIC KEY-----".format(pem_b64) 1965 | result['pem'] = pem_str 1966 | 1967 | #fingerprint (version 3) 1968 | if version == 3: 1969 | #make sure this is an rsa public key 1970 | if algo_id not in [1, 2, 3]: 1971 | result['error'] = True 1972 | result.setdefault("error_msg", []).append( 1973 | "Public Key algorithm ({}) is not RSA, which is required in version 3.".format(algo_id)) 1974 | return result 1975 | body = "{}{}".format(n_bytes, e_bytes) 1976 | result['fingerprint'] = hashlib.md5(body).hexdigest() 1977 | result['key_id'] = result['n'][-16:] 1978 | 1979 | #fingerprint (version 4) 1980 | elif version == 4: 1981 | self.rawfile.seek(body_start) 1982 | len_hex = "{0:0{1}x}".format(body_len, 4).decode("hex") 1983 | body = "\x99{}{}".format(len_hex, self.rawfile.read(body_len)) 1984 | result['fingerprint'] = hashlib.sha1(body).hexdigest() 1985 | result['key_id'] = result['fingerprint'][-16:] 1986 | 1987 | return result 1988 | 1989 | def generate_pubkey(self, p): 1990 | #version 1991 | bytes = "{0:0{1}x}".format(p['version'], 2).decode("hex") 1992 | 1993 | #creation time 1994 | bytes += "{0:0{1}x}".format(p['creation_time'], 8).decode("hex") 1995 | 1996 | #valid days 1997 | if p['version'] == 3: 1998 | bytes += "{0:0{1}x}".format(p['valid_days'], 4).decode("hex") 1999 | 2000 | #algo_id 2001 | bytes += "{0:0{1}x}".format(p['algo_id'], 2).decode("hex") 2002 | 2003 | #RSA 2004 | if p['algo_id'] in [1, 2, 3]: 2005 | 2006 | #modulus 2007 | modulus_int = int(p['n'], 16) 2008 | bytes += "{0:0{1}x}".format(modulus_int.bit_length(), 4).decode("hex") 2009 | bytes += p['n'].decode("hex") 2010 | 2011 | #exponent 2012 | exponent_int = int(p['e'], 16) 2013 | bytes += "{0:0{1}x}".format(exponent_int.bit_length(), 4).decode("hex") 2014 | bytes += p['e'].decode("hex") 2015 | 2016 | #Elgamal 2017 | elif p['algo_id'] in [16, 20]: 2018 | raise NotImplementedError("Elgamal public key parsing is not implemented yet :(") 2019 | 2020 | #DSA 2021 | elif p['algo_id'] == 17: 2022 | raise NotImplementedError("DSA public key parsing is not implemented yet :(") 2023 | 2024 | #ECDH 2025 | elif p['algo_id'] == 18: 2026 | raise NotImplementedError("ECDH public key parsing is not implemented yet :(") 2027 | 2028 | #ECDSA 2029 | elif p['algo_id'] == 19: 2030 | raise NotImplementedError("ECDSA public key parsing is not implemented yet :(") 2031 | 2032 | #DH 2033 | elif p['algo_id'] == 21: 2034 | raise NotImplementedError("DH public key parsing is not implemented yet :(") 2035 | 2036 | return bytes 2037 | 2038 | def read_pubsubkey(self, body_start, body_len): 2039 | """ 2040 | Specification: 2041 | https://tools.ietf.org/html/rfc4880#section-5.5.1.2 2042 | 2043 | Return Format: 2044 | Same as read_pubkey, except tag_id is 14 and tag_name is "Public-Subkey" 2045 | """ 2046 | pubkey_result = self.read_pubkey(body_start, body_len) 2047 | pubkey_result['tag_id'] = 14 2048 | pubkey_result['tag_name'] = "Public-Subkey" 2049 | return pubkey_result 2050 | 2051 | def generate_pubsubkey(self, p): 2052 | return self.generate_pubkey(p) 2053 | 2054 | def read_userid(self, body_start, body_len): 2055 | """ 2056 | Specification: 2057 | https://tools.ietf.org/html/rfc4880#section-5.11 2058 | 2059 | Return Format: 2060 | { 2061 | #standard packet values 2062 | "tag_id": 6, 2063 | "tag_name": "User ID", 2064 | "body_start": 0, 2065 | "body_len": 123, 2066 | 2067 | #errors (if any) 2068 | "error": True, 2069 | "error_msg": ["Error msg 1", "Error msg 2"], 2070 | 2071 | #User ID specific fields 2072 | "user_id": "John Doe (johndoe1234) ", 2073 | } 2074 | """ 2075 | self.rawfile.seek(body_start) 2076 | return { 2077 | "tag_id": 13, 2078 | "tag_name": "User ID", 2079 | "body_start": body_start, 2080 | "body_len": body_len, 2081 | "user_id": self.rawfile.read(body_len), 2082 | } 2083 | 2084 | def generate_userid(self, p): 2085 | return p['user_id'] 2086 | 2087 | def read_attribute(self, body_start, body_len): 2088 | """ 2089 | Specification: 2090 | https://tools.ietf.org/html/rfc4880#section-5.12 2091 | 2092 | Return Format: 2093 | { 2094 | #standard packet values 2095 | "tag_id": 17, 2096 | "tag_name": "User Attribute", 2097 | "body_start": 0, 2098 | "body_len": 123, 2099 | 2100 | #errors (if any) 2101 | "error": True, 2102 | "error_msg": ["Error msg 1", "Error msg 2"], 2103 | 2104 | #User Attribute specific fields 2105 | "subpackets": [ 2106 | { 2107 | #standard subpacket values 2108 | "type_id": 1, 2109 | "type_name": "Image", 2110 | 2111 | #errors (if any) 2112 | "error": True, 2113 | "error_msg": ["Error msg 1", "Error msg 2"], 2114 | 2115 | #image specific values 2116 | "version": 1, 2117 | "encoding": "JPEG", 2118 | "image": "", 2119 | }, 2120 | ... 2121 | ], 2122 | } 2123 | """ 2124 | result = { 2125 | "tag_id": 17, 2126 | "tag_name": "User Attribute", 2127 | "body_start": body_start, 2128 | "body_len": body_len, 2129 | "subpackets": [], 2130 | } 2131 | 2132 | #read the user attribute subpackets 2133 | self.rawfile.seek(body_start) 2134 | while self.rawfile.tell() < (body_start + body_len): 2135 | 2136 | #one byte length 2137 | first_octet = ord(self.rawfile.read(1)) 2138 | if first_octet < 192: 2139 | subpacket_len = first_octet 2140 | 2141 | #two bytes length 2142 | elif first_octet >= 192 and first_octet < 255: 2143 | second_octet = ord(self.rawfile.read(1)) 2144 | subpacket_len = ((first_octet - 192) << 8) + second_octet + 192 2145 | 2146 | #four bytes length 2147 | elif first_octet == 255: 2148 | four_bytes = self.rawfile.read(4) 2149 | subpacket_len = int(four_bytes.encode('hex'), 16) 2150 | 2151 | #make sure there's no overflow 2152 | if self.rawfile.tell() + subpacket_len > body_start + body_len: 2153 | result['error'] = True 2154 | result.setdefault("error_msg", []).append("User Attribute subpacket overflows the overall length.") 2155 | subpacket_len = max(0, body_len - (self.rawfile.tell() - body_start)) 2156 | 2157 | #subpacket type 2158 | type_id = ord(self.rawfile.read(1)) 2159 | subpacket = {"type_id": type_id} 2160 | try: 2161 | type_name = { 2162 | 1: "Image", 2163 | 100: "Private or experimental", 2164 | 101: "Private or experimental", 2165 | 102: "Private or experimental", 2166 | 103: "Private or experimental", 2167 | 104: "Private or experimental", 2168 | 105: "Private or experimental", 2169 | 106: "Private or experimental", 2170 | 107: "Private or experimental", 2171 | 108: "Private or experimental", 2172 | 109: "Private or experimental", 2173 | 110: "Private or experimental", 2174 | }[type_id] 2175 | except KeyError: 2176 | result['error'] = True 2177 | result.setdefault("error_msg", []).append("User Attribute subpacket type ({}) not recognized.".format(type_id)) 2178 | subpacket['error'] = True 2179 | subpacket.setdefault("error_msg", []).append("User Attribute subpacket type ({}) not recognized.".format(type_id)) 2180 | type_name = "Unknown" 2181 | subpacket['type_name'] = type_name 2182 | 2183 | #Image subpacket 2184 | if type_id == 1: 2185 | 2186 | #get the header length 2187 | header_len_bytes = "".join(reversed(self.rawfile.read(2))) #little endian 2188 | header_len = int(header_len_bytes.encode('hex'), 16) 2189 | if header_len != 16: 2190 | result['error'] = True 2191 | result.setdefault("error_msg", []).append("Image header size is invalid ({}).".format(header_len)) 2192 | subpacket['error'] = True 2193 | subpacket.setdefault("error_msg", []).append("Image header size is invalid ({}).".format(header_len)) 2194 | result['subpackets'].append(subpacket) 2195 | return result 2196 | 2197 | #get the header version 2198 | header_version = ord(self.rawfile.read(1)) 2199 | if header_version != 1: 2200 | result['error'] = True 2201 | result.setdefault("error_msg", []).append("Image header version is invalid ({}).".format(header_version)) 2202 | subpacket['error'] = True 2203 | subpacket.setdefault("error_msg", []).append("Image header version is invalid ({}).".format(header_version)) 2204 | subpacket['version'] = header_version 2205 | 2206 | #get image encoding 2207 | image_encoding_id = ord(self.rawfile.read(1)) 2208 | try: 2209 | image_encoding = { 2210 | 1: "JPEG", 2211 | }[image_encoding_id] 2212 | except KeyError: 2213 | image_encoding = "Unknown" 2214 | result['error'] = True 2215 | result.setdefault("error_msg", []).append("Image encoding ({}) not recognized.".format(image_encoding_id)) 2216 | subpacket['error'] = True 2217 | subpacket.setdefault("error_msg", []).append("Image encoding ({}) not recognized.".format(image_encoding_id)) 2218 | subpacket['encoding'] = image_encoding 2219 | 2220 | #the rest of the header is blank 2221 | header_remaining = self.rawfile.read(12) 2222 | if int(header_remaining.encode('hex'), 16) != 0: 2223 | result['error'] = True 2224 | result.setdefault("error_msg", []).append("Image header remainder contains non-zero values.") 2225 | 2226 | #the rest of the subpacket is the image 2227 | image_raw = self.rawfile.read(subpacket_len - header_len - 1) 2228 | subpacket['image'] = base64.b64encode(image_raw) 2229 | 2230 | result['subpackets'].append(subpacket) 2231 | 2232 | return result 2233 | 2234 | def generate_attribute(self, p): 2235 | #build subpackets 2236 | attr_bytes = "" 2237 | subpackets = [] 2238 | for sp in p['subpackets']: 2239 | sp_bytes = "" 2240 | 2241 | #images 2242 | if sp['type_id'] == 1: 2243 | 2244 | #encoding 2245 | encoding_id = { 2246 | "JPEG": 1, 2247 | "Unknown": 0, 2248 | }[sp['encoding']] 2249 | 2250 | #image subpacket 2251 | sp_bytes += "{h_len}{h_ver}{encoding}{pad}{img}".format( 2252 | #header length (16 bytes, little endian) 2253 | h_len="1000".decode("hex"), 2254 | #header version 2255 | h_ver="{0:0{1}x}".format(sp['version'], 2).decode("hex"), 2256 | #encoding 2257 | encoding="{0:0{1}x}".format(encoding_id, 2).decode("hex"), 2258 | #remainder of header length 2259 | pad="{0:0{1}x}".format(0, 24).decode("hex"), 2260 | #raw image 2261 | img=base64.b64decode(sp['image']), 2262 | ) 2263 | 2264 | #calculate subpacket length (same a signature subpacket lengths) 2265 | sp_header = "" 2266 | sp_len = len(sp_bytes) + 1 2267 | 2268 | #one byte length 2269 | if sp_len < 192: 2270 | sp_header += "{0:0{1}x}".format(sp_len, 2).decode("hex") 2271 | 2272 | #two bytes length 2273 | elif sp_len >= 192 and sp_len <= 8383: 2274 | octets = (sp_len - 192) | 0xC000 2275 | sp_header += "{0:0{1}x}".format(octets, 4).decode("hex") 2276 | 2277 | #five bytes length 2278 | elif sp_len > 8383: 2279 | sp_header += "ff{0:0{1}x}".format(sp_len, 8).decode("hex") 2280 | 2281 | #add type 2282 | sp_header += "{0:0{1}x}".format(sp['type_id'], 2).decode("hex") 2283 | 2284 | #add to overall attribute bytes 2285 | attr_bytes += sp_header + sp_bytes 2286 | 2287 | return attr_bytes 2288 | 2289 | def read_compressed(self, body_start, body_len): 2290 | """ 2291 | Specification: 2292 | https://tools.ietf.org/html/rfc4880#section-5.6 2293 | https://tools.ietf.org/html/rfc4880#section-9.3 2294 | 2295 | Compression Algorithms: 2296 | 0 - Uncompressed 2297 | 1 - ZIP 2298 | 2 - ZLIB 2299 | 3 - BZip2 2300 | 100 to 110 - Private/Experimental algorithm 2301 | 2302 | Return Format: 2303 | { 2304 | #standard packet values 2305 | "tag_id": 8, 2306 | "tag_name": "Compressed Data", 2307 | "body_start": 0, 2308 | "body_len": 123, 2309 | 2310 | #errors (if any) 2311 | "error": True, 2312 | "error_msg": ["Error msg 1", "Error msg 2"], 2313 | 2314 | #decompressed packets 2315 | "compression_algo_id": 1, 2316 | "compression_algo_name": "ZIP", 2317 | "packets": [ 2318 | {...}, 2319 | ... 2320 | ], 2321 | } 2322 | """ 2323 | result = { 2324 | "tag_id": 8, 2325 | "tag_name": "Compressed Data", 2326 | "body_start": body_start, 2327 | "body_len": body_len, 2328 | "packets": [], 2329 | } 2330 | 2331 | #read the compression algo 2332 | self.rawfile.seek(body_start) 2333 | compression_algo_id = ord(self.rawfile.read(1)) 2334 | result['compression_algo_id'] = compression_algo_id 2335 | try: 2336 | compression_algo_name = { 2337 | 0: "Uncompressed", 2338 | 1: "ZIP", 2339 | 2: "ZLIB", 2340 | 3: "BZip2", 2341 | 100: "Private or experimental", 2342 | 101: "Private or experimental", 2343 | 102: "Private or experimental", 2344 | 103: "Private or experimental", 2345 | 104: "Private or experimental", 2346 | 105: "Private or experimental", 2347 | 106: "Private or experimental", 2348 | 107: "Private or experimental", 2349 | 108: "Private or experimental", 2350 | 109: "Private or experimental", 2351 | 110: "Private or experimental", 2352 | }[compression_algo_id] 2353 | result['compression_algo_name'] = compression_algo_name 2354 | except KeyError: 2355 | result['error'] = True 2356 | result.setdefault("error_msg", []).append("Compression algorithm ({}) not recognized.".format(compression_algo_id)) 2357 | return result 2358 | 2359 | #Uncompressed 2360 | if compression_algo_id == 0: 2361 | decomp_file = tempfile.NamedTemporaryFile() 2362 | decomp_file.write(self.rawfile.read(body_len-1)) 2363 | 2364 | #ZIP 2365 | elif compression_algo_id == 1: 2366 | decompress = zlib.decompressobj(-zlib.MAX_WBITS) 2367 | decompress_raw = decompress.decompress(self.rawfile.read(body_len-1)) 2368 | decomp_file = tempfile.NamedTemporaryFile() 2369 | decomp_file.write(decompress_raw) 2370 | decomp_file.write(decompress.flush()) 2371 | 2372 | #ZLIB 2373 | elif compression_algo_id == 2: 2374 | decomp_file = tempfile.NamedTemporaryFile() 2375 | decomp_file.write(zlib.decompress(self.rawfile.read(body_len-1))) 2376 | 2377 | #BZip2 2378 | elif compression_algo_id == 3: 2379 | decomp_file = tempfile.NamedTemporaryFile() 2380 | decomp_file.write(bz2.decompress(self.rawfile.read(body_len-1))) 2381 | 2382 | #find uncompressed length 2383 | decomp_len = decomp_file.tell() 2384 | result['decompressed_len'] = decomp_len 2385 | 2386 | #parse the decompressed file 2387 | decomp_file.seek(0) 2388 | for packet in OpenPGPFile(decomp_file): 2389 | result['packets'].append(packet) 2390 | 2391 | return result 2392 | 2393 | def read_literal(self, body_start, body_len): 2394 | """ 2395 | Specification: 2396 | https://tools.ietf.org/html/rfc4880#section-5.9 2397 | 2398 | Return Format: 2399 | { 2400 | #standard packet values 2401 | "tag_id": 8, 2402 | "tag_name": "Compressed Data", 2403 | "body_start": 0, 2404 | "body_len": 123, 2405 | 2406 | #errors (if any) 2407 | "error": True, 2408 | "error_msg": ["Error msg 1", "Error msg 2"], 2409 | 2410 | #decompressed packets 2411 | "mode": "b", #"b", "t", or "u", 2412 | "filename": "myfile", 2413 | "timestamp": 1234567890, 2414 | "data": "...", 2415 | } 2416 | """ 2417 | result = { 2418 | "tag_id": 11, 2419 | "tag_name": "Literal Data", 2420 | "body_start": body_start, 2421 | "body_len": body_len, 2422 | } 2423 | 2424 | #read the mode type 2425 | self.rawfile.seek(body_start) 2426 | result['mode'] = self.rawfile.read(1) 2427 | if result['mode'] not in ['b', 't', 'u']: 2428 | result['error'] = True 2429 | result.setdefault("error_msg", []).append("Data mode ({}) not recognized.".format(result['mode'])) 2430 | 2431 | #read the filename 2432 | filename_len = ord(self.rawfile.read(1)) 2433 | filename = self.rawfile.read(filename_len) 2434 | result['filename'] = filename 2435 | 2436 | #read the timestamp 2437 | timestamp = int(self.rawfile.read(4).encode('hex'), 16) 2438 | result['timestamp'] = timestamp 2439 | 2440 | #read the literal data 2441 | result['data'] = self.rawfile.read(body_len - 1 - 4 - 1 - filename_len) 2442 | 2443 | return result 2444 | 2445 | def read_packets(self): 2446 | """ 2447 | Specification: 2448 | https://tools.ietf.org/html/rfc4880#section-4.2 2449 | https://tools.ietf.org/html/rfc4880#section-4.3 2450 | 2451 | Packet tag ids: 2452 | 0 -- Reserved - a packet tag MUST NOT have this value 2453 | 1 -- Public-Key Encrypted Session Key Packet 2454 | 2 -- Signature Packet 2455 | 3 -- Symmetric-Key Encrypted Session Key Packet 2456 | 4 -- One-Pass Signature Packet 2457 | 5 -- Secret-Key Packet 2458 | 6 -- Public-Key Packet 2459 | 7 -- Secret-Subkey Packet 2460 | 8 -- Compressed Data Packet 2461 | 9 -- Symmetrically Encrypted Data Packet 2462 | 10 -- Marker Packet 2463 | 11 -- Literal Data Packet 2464 | 12 -- Trust Packet 2465 | 13 -- User ID Packet 2466 | 14 -- Public-Subkey Packet 2467 | 17 -- User Attribute Packet 2468 | 18 -- Sym. Encrypted and Integrity Protected Data Packet 2469 | 19 -- Modification Detection Code Packet 2470 | 60 to 63 -- Private or Experimental Values 2471 | """ 2472 | 2473 | #get the file length 2474 | self.rawfile.seek(0, os.SEEK_END) 2475 | filelen = self.rawfile.tell() 2476 | 2477 | #go through and find all the packet headers 2478 | i = 0 2479 | while True: 2480 | 2481 | #break when at the end of the file 2482 | if i >= filelen: 2483 | break 2484 | 2485 | #OpenPGP packet header byte 2486 | self.rawfile.seek(i) 2487 | packet_start = i 2488 | packet_header = ord(self.rawfile.read(1)) 2489 | 2490 | #Bit 7 = packet header (must be 1) 2491 | packet_header_check = (packet_header & 0x80) >> 7 2492 | if packet_header_check != 1: 2493 | raise ValueError("Invalid packet header at byte {} (value=0x{:02x}).".format(i, packet_header)) 2494 | 2495 | #Bit 6 = packet_format 2496 | packet_format = (packet_header & 0x40) >> 6 2497 | 2498 | #new packet format 2499 | if packet_format == 1: 2500 | 2501 | #Bits 5-0 = packet tag 2502 | packet_tag = (packet_header & 0x3f) 2503 | 2504 | #one byte length 2505 | first_octet = ord(self.rawfile.read(1)) 2506 | if first_octet < 192: 2507 | body_len = first_octet 2508 | 2509 | #two bytes length 2510 | elif first_octet >= 192 and first_octet < 224: 2511 | second_octet = ord(self.rawfile.read(1)) 2512 | body_len = ((first_octet - 192) << 8) + second_octet + 192 2513 | 2514 | #five byte length 2515 | elif first_octet == 255: 2516 | four_bytes = self.rawfile.read(4) 2517 | body_len = int(four_bytes.encode('hex'), 16) 2518 | 2519 | #partial length 2520 | #TODO: handle length bytes in the middle of the packet 2521 | else: 2522 | body_len = 1 << (first_octet & 0x1f) 2523 | 2524 | #loop until end of packet is reached 2525 | original_i = self.rawfile.tell() 2526 | next_i = original_i + body_len 2527 | while True: 2528 | self.rawfile.seek(next_i) 2529 | next_len = self.rawfile.read(1) 2530 | first_octet = ord(next_len) 2531 | if first_octet < 192: 2532 | body_len += first_octet + 1 2533 | break 2534 | elif first_octet >= 192 and first_octet < 224: 2535 | second_octet = ord(self.rawfile.read(1)) 2536 | body_len += ((first_octet - 192) << 8) + second_octet + 192 + 2 2537 | break 2538 | elif first_octet == 255: 2539 | four_bytes = self.rawfile.read(4) 2540 | body_len += int(four_bytes.encode('hex'), 16) + 5 2541 | break 2542 | else: 2543 | chunk_len = 1 << (first_octet & 0x1f) 2544 | body_len += chunk_len + 1 2545 | next_i += chunk_len + 1 2546 | if next_i > filelen: 2547 | body_len = body_len - (next_i - filelen) 2548 | break 2549 | self.rawfile.seek(original_i) 2550 | 2551 | #old packet format 2552 | elif packet_format == 0: 2553 | 2554 | #Bits 5-2 = packet tag 2555 | packet_tag = (packet_header & 0x3c) >> 2 2556 | 2557 | #Bits 1-0 = packet length type 2558 | packet_lentype = (packet_header & 0x03) 2559 | 2560 | #Get packet length based on length type 2561 | if packet_lentype == 0: 2562 | #one byte length 2563 | lenbytes = self.rawfile.read(1) 2564 | body_len = ord(lenbytes) 2565 | 2566 | elif packet_lentype == 1: 2567 | #two bytes length 2568 | lenbytes = self.rawfile.read(2) 2569 | body_len = int(lenbytes.encode('hex'), 16) 2570 | 2571 | elif packet_lentype == 2: 2572 | #four bytes length 2573 | lenbytes = self.rawfile.read(4) 2574 | body_len = int(lenbytes.encode('hex'), 16) 2575 | 2576 | elif packet_lentype == 3: 2577 | #indeterminate length (i.e. to end of file) 2578 | self.rawfile.seek(0, os.SEEK_END) 2579 | body_len = self.rawfile.tell() - i - 1 2580 | self.rawfile.seek(i + 1) 2581 | 2582 | #get the packet bytes 2583 | i = self.rawfile.tell() 2584 | 2585 | #TODO: Public-Key Encrypted Session Key Packet 2586 | if packet_tag == 1: 2587 | raise NotImplementedError("Public-Key Encrypted Session Key Packet is not implemented yet :(") 2588 | 2589 | #Signature Packet 2590 | elif packet_tag == 2: 2591 | packet_dict = self.read_signature(i, body_len) 2592 | 2593 | #TODO: Symmetric-Key Encrypted Session Key Packet 2594 | elif packet_tag == 3: 2595 | raise NotImplementedError("Symmetric-Key Encrypted Session Key Packet is not implemented yet :(") 2596 | 2597 | #One-Pass Signature Packet 2598 | elif packet_tag == 4: 2599 | packet_dict = self.read_onepasssig(i, body_len) 2600 | 2601 | #TODO: Secret-Key Packet 2602 | elif packet_tag == 5: 2603 | raise NotImplementedError("Secret-Key Packet is not implemented yet :(") 2604 | 2605 | #Public-Key Packet 2606 | elif packet_tag == 6: 2607 | packet_dict = self.read_pubkey(i, body_len) 2608 | 2609 | #TODO: Secret-Subkey Packet 2610 | elif packet_tag == 7: 2611 | raise NotImplementedError("Secret-Subkey Packet is not implemented yet :(") 2612 | 2613 | #Compressed Data Packet 2614 | elif packet_tag == 8: 2615 | packet_dict = self.read_compressed(i, body_len) 2616 | 2617 | #TODO: Symmetrically Encrypted Data Packet 2618 | elif packet_tag == 9: 2619 | raise NotImplementedError("Symmetrically Encrypted Data Packet is not implemented yet :(") 2620 | 2621 | #TODO: Marker Packet 2622 | elif packet_tag == 10: 2623 | raise NotImplementedError("Marker Packet is not implemented yet :(") 2624 | 2625 | #Literal Data Packet 2626 | elif packet_tag == 11: 2627 | packet_dict = self.read_literal(i, body_len) 2628 | 2629 | #TODO: Trust Packet 2630 | elif packet_tag == 12: 2631 | raise NotImplementedError("Trust Packet is not implemented yet :(") 2632 | 2633 | #User ID Packet 2634 | elif packet_tag == 13: 2635 | packet_dict = self.read_userid(i, body_len) 2636 | 2637 | #Public-Subkey Packet 2638 | elif packet_tag == 14: 2639 | packet_dict = self.read_pubsubkey(i, body_len) 2640 | 2641 | #User Attribute Packet 2642 | elif packet_tag == 17: 2643 | packet_dict = self.read_attribute(i, body_len) 2644 | 2645 | #TODO: Sym. Encrypted and Integrity Protected Data Packet 2646 | elif packet_tag == 18: 2647 | raise NotImplementedError("Sym. Encrypted and Integrity Protected Data Packet is not implemented yet :(") 2648 | 2649 | #TODO: Modification Detection Code Packet 2650 | elif packet_tag == 19: 2651 | raise NotImplementedError("Modification Detection Code Packet is not implemented yet :(") 2652 | 2653 | #TODO: Private or Experimental Values 2654 | elif packet_tag in [60, 61, 62, 63]: 2655 | raise NotImplementedError("Private or Experimental Values are not implemented yet :(") 2656 | 2657 | #all other packet tags are invalid 2658 | else: 2659 | raise ValueError("Invalid packet tag ({}).".format(packet_tag)) 2660 | 2661 | #add packet format 2662 | packet_len = i + body_len - packet_start 2663 | packet_dict['packet_format'] = packet_format 2664 | packet_dict['packet_start'] = packet_start 2665 | packet_dict['packet_len'] = packet_len 2666 | self.rawfile.seek(packet_start) 2667 | packet_dict['packet_raw'] = self.rawfile.read(packet_len).encode("hex") 2668 | self.append(packet_dict) 2669 | 2670 | #iterate to the next packet header 2671 | i += body_len 2672 | 2673 | #return the packets 2674 | return self 2675 | 2676 | def generate_packets(self): 2677 | bytes = "" 2678 | for i in xrange(0, len(self)): 2679 | 2680 | #TODO: Public-Key Encrypted Session Key Packet 2681 | if self[i]['tag_id'] == 1: 2682 | raise NotImplementedError("Public-Key Encrypted Session Key Packet is not implemented yet :(") 2683 | 2684 | #Signature Packet 2685 | elif self[i]['tag_id'] == 2: 2686 | packet_bytes = self.generate_signature(self[i]) 2687 | 2688 | #TODO: Symmetric-Key Encrypted Session Key Packet 2689 | elif self[i]['tag_id'] == 3: 2690 | raise NotImplementedError("Symmetric-Key Encrypted Session Key Packet is not implemented yet :(") 2691 | 2692 | #TODO: One-Pass Signature Packet 2693 | elif self[i]['tag_id'] == 4: 2694 | raise NotImplementedError("One-Pass Signature Packet is not implemented yet :(") 2695 | 2696 | #TODO: Secret-Key Packet 2697 | elif self[i]['tag_id'] == 5: 2698 | raise NotImplementedError("Secret-Key Packet is not implemented yet :(") 2699 | 2700 | #Public-Key Packet 2701 | elif self[i]['tag_id'] == 6: 2702 | packet_bytes = self.generate_pubkey(self[i]) 2703 | 2704 | #TODO: Secret-Subkey Packet 2705 | elif self[i]['tag_id'] == 7: 2706 | raise NotImplementedError("Secret-Subkey Packet is not implemented yet :(") 2707 | 2708 | #TODO: Compressed Data Packet 2709 | elif self[i]['tag_id'] == 8: 2710 | raise NotImplementedError("Compressed Data Packet is not implemented yet :(") 2711 | 2712 | #TODO: Symmetrically Encrypted Data Packet 2713 | elif self[i]['tag_id'] == 9: 2714 | raise NotImplementedError("Symmetrically Encrypted Data Packet is not implemented yet :(") 2715 | 2716 | #TODO: Marker Packet 2717 | elif self[i]['tag_id'] == 10: 2718 | raise NotImplementedError("Marker Packet is not implemented yet :(") 2719 | 2720 | #TODO: Literal Data Packet 2721 | elif self[i]['tag_id'] == 11: 2722 | raise NotImplementedError("Literal Data Packet is not implemented yet :(") 2723 | 2724 | #TODO: Trust Packet 2725 | elif self[i]['tag_id'] == 12: 2726 | raise NotImplementedError("Trust Packet is not implemented yet :(") 2727 | 2728 | #User ID Packet 2729 | elif self[i]['tag_id'] == 13: 2730 | packet_bytes = self.generate_userid(self[i]) 2731 | 2732 | #Public-Subkey Packet 2733 | elif self[i]['tag_id'] == 14: 2734 | packet_bytes = self.generate_pubsubkey(self[i]) 2735 | 2736 | #User Attribute Packet 2737 | elif self[i]['tag_id'] == 17: 2738 | packet_bytes = self.generate_attribute(self[i]) 2739 | 2740 | #TODO: Sym. Encrypted and Integrity Protected Data Packet 2741 | elif self[i]['tag_id'] == 18: 2742 | raise NotImplementedError("Sym. Encrypted and Integrity Protected Data Packet is not implemented yet :(") 2743 | 2744 | #TODO: Modification Detection Code Packet 2745 | elif self[i]['tag_id'] == 19: 2746 | raise NotImplementedError("Modification Detection Code Packet is not implemented yet :(") 2747 | 2748 | #TODO: Private or Experimental Values 2749 | elif self[i]['tag_id'] in [60, 61, 62, 63]: 2750 | raise NotImplementedError("Private or Experimental Values are not implemented yet :(") 2751 | 2752 | #all other packet tags are invalid 2753 | else: 2754 | raise ValueError("Invalid packet tag ({}).".format(self[i]['tag_id'])) 2755 | 2756 | #header bytes 2757 | header = "" 2758 | packet_len = len(packet_bytes) 2759 | 2760 | #new packet format 2761 | if self[i]['packet_format'] == 1: 2762 | header_byte = 0x80 | 0x40 | self[i]['tag_id'] 2763 | header += "{0:0{1}x}".format(header_byte, 2).decode("hex") 2764 | 2765 | #one byte length 2766 | if packet_len < 192: 2767 | header += "{0:0{1}x}".format(packet_len, 2).decode("hex") 2768 | 2769 | #two bytes length 2770 | elif packet_len >= 192 and packet_len <= 8383: 2771 | octets = (packet_len - 192) | 0xC000 2772 | header += "{0:0{1}x}".format(octets, 4).decode("hex") 2773 | 2774 | #five bytes length 2775 | elif packet_len > 8383: 2776 | header += "ff{0:0{1}x}".format(packet_len, 8).decode("hex") 2777 | 2778 | #old packet format 2779 | if self[i]['packet_format'] == 0: 2780 | header_byte = 0x80 | 0x00 | (self[i]['tag_id'] << 2) 2781 | 2782 | #one byte length 2783 | if packet_len < 256: 2784 | header_byte = header_byte | 0x00 2785 | header += "{0:0{1}x}".format(header_byte, 2).decode("hex") 2786 | header += "{0:0{1}x}".format(packet_len, 2).decode("hex") 2787 | 2788 | #two bytes length 2789 | elif packet_len < 65535: 2790 | header_byte = header_byte | 0x01 2791 | header += "{0:0{1}x}".format(header_byte, 2).decode("hex") 2792 | header += "{0:0{1}x}".format(packet_len, 4).decode("hex") 2793 | 2794 | #four bytes length 2795 | else: 2796 | header_byte = header_byte | 0x10 2797 | header += "{0:0{1}x}".format(header_byte, 2).decode("hex") 2798 | header += "{0:0{1}x}".format(packet_len, 8).decode("hex") 2799 | 2800 | bytes += header + packet_bytes 2801 | 2802 | return bytes 2803 | 2804 | def armor_to_bytes(self, fileobj): 2805 | """Convert a armored file to non-base64 encoded byte file""" 2806 | 2807 | #armored message regex pattern 2808 | armor_re = re.compile("\ 2809 | (?:\ 2810 | ^-----BEGIN PGP SIGNED MESSAGE-----\n\ 2811 | (?P.*?)\n\n\ 2812 | (?P.*?)\n\ 2813 | |)\ 2814 | ^-----BEGIN PGP \ 2815 | (?P\ 2816 | (?P(?:PUBLIC|PRIVATE) KEY BLOCK)\ 2817 | |\ 2818 | (?PMESSAGE(?: PART (?P[0-9]+)(?:\/(?P[0-9]+)|)|))\ 2819 | |\ 2820 | (?PSIGNATURE)\ 2821 | )\ 2822 | ----- *\n+ *\ 2823 | (?P(?:[^:\n]+: [^\n]* *\n+ *)*) *\n+ *\ 2824 | (?P.*?) *\n+ *\ 2825 | (?P=[A-Za-z0-9\+\/]{4}) *\n+ *\ 2826 | -----END PGP \ 2827 | (?P\ 2828 | (?P(?:PUBLIC|PRIVATE) KEY BLOCK)\ 2829 | |\ 2830 | (?PMESSAGE(?: PART (?P[0-9]+)(?:\/(?P[0-9]+)|)|))\ 2831 | |\ 2832 | (?PSIGNATURE)\ 2833 | )\ 2834 | -----$", re.MULTILINE|re.DOTALL) 2835 | 2836 | #adapted from http://stackoverflow.com/a/4544284 2837 | def _crc24(chars): 2838 | INIT = 0xB704CE 2839 | POLY = 0x864CFB 2840 | crc = INIT 2841 | for c in map(ord, chars): 2842 | crc ^= (c << 16) 2843 | for i in xrange(8): 2844 | crc <<= 1 2845 | if crc & 0x1000000: crc ^= POLY 2846 | return crc & 0xFFFFFF 2847 | 2848 | #go through the file and try to find the armored text 2849 | outfile = cStringIO.StringIO() 2850 | fileobj.seek(0) 2851 | file_str = fileobj.read() 2852 | for chunk in armor_re.finditer(file_str): 2853 | 2854 | #make sure the start and end match 2855 | chunk = chunk.groupdict() 2856 | if chunk['start'] != chunk['end']: 2857 | continue 2858 | 2859 | #convert the data64 to bytes 2860 | bytes64 = chunk['data64'].replace(" ", "").replace("\n", "") 2861 | bytes = base64.b64decode(bytes64) 2862 | 2863 | #test the checksum 2864 | checksum = int(base64.b64decode(chunk['checksum']).encode("hex"), 16) 2865 | checksum_verify = _crc24(bytes) 2866 | if checksum_verify != checksum: 2867 | raise ValueError("Armor checksum ({}) does not match body ({})".format( 2868 | "{0:0{1}x}".format(checksum, 4), 2869 | "{0:0{1}x}".format(checksum_verify, 4))) 2870 | 2871 | #found some armored packets, so add to the output 2872 | outfile.write(bytes) 2873 | 2874 | #return the file-like object 2875 | outfile.seek(0) 2876 | return outfile 2877 | 2878 | if __name__ == "__main__": 2879 | import sys 2880 | import glob 2881 | import json 2882 | from copy import copy 2883 | from argparse import ArgumentParser 2884 | from argparse import RawTextHelpFormatter 2885 | 2886 | parser = ArgumentParser( 2887 | formatter_class=RawTextHelpFormatter, 2888 | description=""" 2889 | Output a pgp file's packets into rows of json-formatted objects. 2890 | 2891 | NOTE: Each row of output is a json object, but the whole output' 2892 | itself is not a json list. 2893 | 2894 | Add the --merge-public-keys (-m) to roll up public keys. This is 2895 | helpful if you are working with a dumped keyserver database that 2896 | is just a huge list of concatenated packets (public keys, 2897 | signatures, subkeys, etc.). 2898 | 2899 | Examples: 2900 | python openpgp.py /home/me/Alice.pub 2901 | python openpgp.py --merge-public-keys "/tmp/dump*.pgp" | gzip > pubkeys.json 2902 | """) 2903 | parser.add_argument("file", nargs="+", help="the pgp file(s)") 2904 | parser.add_argument("-m", "--merge-public-keys", action="store_true", help="roll up public key packets") 2905 | args = parser.parse_args() 2906 | 2907 | #iterate through file list 2908 | for f in args.file: 2909 | paths = glob.glob(os.path.expanduser(f)) 2910 | for path in paths: 2911 | 2912 | #parse the packets 2913 | sys.stderr.write("Parsing {}...".format(path)) 2914 | sys.stderr.flush() 2915 | 2916 | packets = OpenPGPFile(open(path)) 2917 | 2918 | sys.stderr.write("Done\n") 2919 | sys.stderr.flush() 2920 | 2921 | #merge public keys 2922 | if args.merge_public_keys: 2923 | 2924 | sys.stderr.write("Dumping public keys...".format(path)) 2925 | sys.stderr.flush() 2926 | 2927 | key = {} 2928 | for p in packets: 2929 | if p['tag_name'] == "Public-Key": 2930 | if key: 2931 | print json.dumps(key, sort_keys=True, encoding="latin1") 2932 | key = copy(p) 2933 | else: 2934 | #bubble errors 2935 | if p.get("error"): 2936 | key['error'] = True 2937 | key.setdefault("error_msg", []).append(p['error_msg']) 2938 | key.setdefault("packets", []).append(p) 2939 | print json.dumps(key, sort_keys=True, encoding="latin1") 2940 | 2941 | sys.stderr.write("Done\n") 2942 | sys.stderr.flush() 2943 | 2944 | #straight dump of packets 2945 | else: 2946 | 2947 | sys.stderr.write("Dumping packets...".format(path)) 2948 | sys.stderr.flush() 2949 | 2950 | for p in packets: 2951 | print json.dumps(p, sort_keys=True, encoding="latin1") 2952 | 2953 | sys.stderr.write("Done\n") 2954 | sys.stderr.flush() 2955 | 2956 | --------------------------------------------------------------------------------