commit 03af5634c15e9908bb2062b8aa1645f3509dfbe0 Author: libre-net-society Date: Tue Aug 30 20:25:23 2016 +0300 initialize repository diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a27a081 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +__pycache__ +*.pyc +venv/ +transports.ini diff --git a/README.md b/README.md new file mode 100644 index 0000000..b5fd844 --- /dev/null +++ b/README.md @@ -0,0 +1,70 @@ +pyseeder +======== + +Reseed data managment tools for I2P + +* Generate reseed signing keypair +* Make reseed data files (su3) +* Download su3 files from official servers for mirroring +* Upload reseed data to different places (with plugins) + +Reseed transports are implemented so that users can bootstrap their I2P nodes +without needing to connect to "official" I2P reseeds. This makes I2P more +invisible for firewalls. + + +Install requirements +-------------------- + + # apt-get install python3 python3-pip + # pip3 install -r requirements.txt + + +Usage +----- + + $ python3 pyseeder.py --help + $ python3 pyseeder.py keygen --help + + +Generating keypair +------------------ + + + $ pyhon3 pyseeder.py keygen --cert data/user_at_mail.i2p.crt --private-key data/priv_key.pem --signer-id user@mail.i2p + +This will generate certificate (user\_at\_mail.i2p.crt) and private RSA key +(priv\_key.pem) in data folder. E-mail is used as certificate identifier. + +Script will prompt for private key password. + + +Generating reseed data +---------------------- + + + $ YOUR_PASSWORD="Pa55w0rd" + $ echo $YOUR_PASSWORD | python3 pyseeder.py reseed --netdb /path/to/netDb --private-key data/priv_key.pem --outfile output/i2pseeds.su3 --signer-id user@mail.i2p + +This will generate file i2pseeds.su3 in output folder, using user@mail.i2p as +certificate identifier. + +Note: you'll have to enter your private key password to stdin, the above +is one of the ways to do it (for cron and scripts). + + +Download su3 file from official servers +--------------------------------------- + + $ python3 pyseeder.py transport.pull --urls https://reseed.i2p-projekt.de/ https://reseed.i2p.vzaws.com:8443/ --outfile output/i2pseeds.su3 + +Note: --urls parameter is optional, defaults are "official" I2P reseeds. + + +Upload su3 file with pluggable transports +----------------------------------------- + + $ python3 pyseeder.py transport.push --config transports.ini --file output/i2pseeds.su3 + +All parameters are optional. Copy file transports.ini.example to +transports.ini. Edit your settings in this new file. diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 0000000..c864d2d --- /dev/null +++ b/data/.gitignore @@ -0,0 +1,2 @@ +*.crt +*.pem diff --git a/output/.gitignore b/output/.gitignore new file mode 100644 index 0000000..29799ed --- /dev/null +++ b/output/.gitignore @@ -0,0 +1 @@ +*.su3 diff --git a/pyseeder.py b/pyseeder.py new file mode 100755 index 0000000..02aa3d5 --- /dev/null +++ b/pyseeder.py @@ -0,0 +1,125 @@ +#! /usr/bin/env python3 +import os, os.path +import sys +from getpass import getpass +import argparse + +from pyseeder.crypto import keygen +from pyseeder.su3file import SU3File +import pyseeder.transport + +from pyseeder.utils import PyseederException + +def keygen_action(args): + """Sub-command to generate keys""" + priv_key_password = getpass("Set private key password: ").encode("utf-8") + keygen(args.cert, args.private_key, priv_key_password, args.signer_id) + +def reseed_action(args): + """Sub-command to generate reseed file""" + priv_key_password = input().encode("utf-8") + su3file = SU3File(args.signer_id) + su3file.reseed(args.netdb) + su3file.write(args.outfile, args.private_key, priv_key_password) + +def transport_pull_action(args): + """Sub-command for downloading su3 file""" + import random + random.shuffle(args.urls) + + for u in args.urls: + if pyseeder.transport.download(u, args.outfile): + return True + + raise PyseederException("Failed to download su3 file") + +def transport_push_action(args): + """Sub-command for uploading su3 file with transports""" + if not os.path.isfile(args.config): + raise PyseederException("Can't read transports config file") + + import configparser + config = configparser.ConfigParser() + config.read(args.config) + pyseeder.transport.upload(args.file, config) + +def main(): + parser = argparse.ArgumentParser() + subparsers = parser.add_subparsers(title="actions", + help="Command to execute") + + kg_parser = subparsers.add_parser( + "keygen", + description="Generates keypair for your reseed", + usage=""" +%(prog)s --cert data/user_at_mail.i2p.crt \\ + --private-key data/priv_key.pem --signer-id user@mail.i2p""" + ) + kg_parser.add_argument("--signer-id", required=True, + help="Identifier of certificate (example: user@mail.i2p)") + kg_parser.add_argument("--private-key", default="data/priv_key.pem", + help="RSA private key (default: data/priv_key.pem)") + kg_parser.add_argument("--cert", required=True, + help="Certificate (example: output/user_at_mail.i2p.crt)") + kg_parser.set_defaults(func=keygen_action) + + + rs_parser = subparsers.add_parser( + "reseed", + description="Creates su3 reseed file", + usage=""" +echo $YOUR_PASSWORD | %(prog)s --netdb /path/to/netDb \\ + --private-key data/priv_key.pem --outfile output/i2pseeds.su3 \\ + --signer-id user@mail.i2p""" + ) + rs_parser.add_argument("--signer-id", required=True, + help="Identifier of certificate (example: user@mail.i2p)") + rs_parser.add_argument("--private-key", default="data/priv_key.pem", + help="RSA private key (default: data/priv_key.pem)") + rs_parser.add_argument("-o", "--outfile", default="output/i2pseeds.su3", + help="Output file (default: output/i2pseeds.su3)") + rs_parser.add_argument("--netdb", required=True, + help="Path to netDb folder (example: ~/.i2pd/netDb)") + rs_parser.set_defaults(func=reseed_action) + + + tpull_parser = subparsers.add_parser( + "transport.pull", + description="Download su3 file from random reseed server", + usage=""" +%(prog)s --urls https://reseed.i2p-projekt.de/ \\ + https://reseed.i2p.vzaws.com:8443/ \\ + --outfile output/i2pseeds.su3""" + ) + tpull_parser.add_argument("--urls", default=pyseeder.transport.RESEED_URLS, + nargs="*", help="""Reseed URLs separated by space, default are + mainline I2P (like https://reseed.i2p-projekt.de/)""") + tpull_parser.add_argument("-o", "--outfile", default="output/i2pseeds.su3", + help="Output file (default: output/i2pseeds.su3)") + tpull_parser.set_defaults(func=transport_pull_action) + + + tpush_parser = subparsers.add_parser( + "transport.push", + description="Upload su3 file with transports", + usage="%(prog)s --config transports.ini --file output/i2pseeds.su3" + ) + tpush_parser.add_argument("--config", default="transports.ini", + help="Transports config file (default: transports.ini)") + tpush_parser.add_argument("-f", "--file", default="output/i2pseeds.su3", + help=".su3 file (default: output/i2pseeds.su3)") + tpush_parser.set_defaults(func=transport_push_action) + + args = parser.parse_args() + if hasattr(args, "func"): + try: + args.func(args) + except PyseederException as pe: + print("Pyseeder error: {}".format(pe)) + sys.exit(1) + else: + parser.print_help() + + +if __name__ == "__main__": + main() diff --git a/pyseeder/__init__.py b/pyseeder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyseeder/crypto.py b/pyseeder/crypto.py new file mode 100644 index 0000000..b0bb9f8 --- /dev/null +++ b/pyseeder/crypto.py @@ -0,0 +1,82 @@ +import os, os.path +import random +import sys +import datetime + +from pyseeder.utils import PyseederException + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography import x509 +from cryptography.x509.oid import NameOID +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding + +def keygen(pub_key, priv_key, priv_key_password, user_id): + """Generate new private key and certificate RSA_SHA512_4096""" + for f in [pub_key, priv_key]: + if not os.access(os.path.dirname(f) or ".", os.W_OK): + raise PyseederException("Can't write {}, access forbidden").format(f) + + # Generate our key + key = rsa.generate_private_key(public_exponent=65537, key_size=4096, + backend=default_backend()) + + # Write our key to disk for safe keeping + with open(priv_key, "wb") as f: + f.write(key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.BestAvailableEncryption( + priv_key_password), + )) + + # Various details about who we are. For a self-signed certificate the + # subject and issuer are always the same. + subject = issuer = x509.Name([ + x509.NameAttribute(NameOID.COUNTRY_NAME, "XX"), + x509.NameAttribute(NameOID.STATE_OR_PROVINCE_NAME, "XX"), + x509.NameAttribute(NameOID.LOCALITY_NAME, "XX"), + x509.NameAttribute(NameOID.ORGANIZATION_NAME, "I2P Anonymous Network"), + x509.NameAttribute(NameOID.ORGANIZATIONAL_UNIT_NAME, "I2P"), + x509.NameAttribute(NameOID.COMMON_NAME, user_id), + ]) + + cert = x509.CertificateBuilder() \ + .subject_name(subject) \ + .issuer_name(issuer) \ + .public_key(key.public_key()) \ + .not_valid_before(datetime.datetime.utcnow()) \ + .not_valid_after( + datetime.datetime.utcnow() + datetime.timedelta(days=365*10) + ) \ + .serial_number(random.randrange(1000000000, 2000000000)) \ + .add_extension( + x509.SubjectKeyIdentifier.from_public_key(key.public_key()), + critical=False, + ).sign(key, hashes.SHA512(), default_backend()) + + with open(pub_key, "wb") as f: + f.write(cert.public_bytes(serialization.Encoding.PEM)) + + +def append_signature(target_file, priv_key, priv_key_password): + """Append signature to the end of file""" + if not os.path.exists(priv_key): + raise PyseederException("Wrong private key path") + + if not os.access(priv_key, os.R_OK): + raise PyseederException("Can't read private key, access forbidden") + + with open(target_file, "rb") as f: + contents = f.read() + + with open(priv_key, "rb") as kf: + private_key = serialization.load_pem_private_key( + kf.read(), password=priv_key_password, backend=default_backend()) + + signature = private_key.sign(contents, padding.PKCS1v15(), hashes.SHA512()) + + with open(target_file, "ab") as f: + f.write(signature) diff --git a/pyseeder/su3file.py b/pyseeder/su3file.py new file mode 100644 index 0000000..fa80238 --- /dev/null +++ b/pyseeder/su3file.py @@ -0,0 +1,84 @@ +import os, os.path +import time, datetime +import random +import io + +from zipfile import ZipFile, ZIP_DEFLATED + +import pyseeder.crypto +from pyseeder.utils import PyseederException + + +class SU3File: + """SU3 file format""" + + def __init__(self, signer_id): + self.SIGNER_ID = signer_id + self.SIGNER_ID_LENGTH = len(self.SIGNER_ID) + self.SIGNATURE_TYPE = 0x0006 + self.SIGNATURE_LENGTH = 512 + self.VERSION_LENGTH = 0x10 + self.FILE_TYPE = None + self.CONTENT_TYPE = None + self.CONTENT = None + self.CONTENT_LENGTH = None + self.VERSION = str(int(time.time())).encode("utf-8") + #self.keytype = "RSA_SHA512_4096" + + def write(self, filename, priv_key, priv_key_password): + """Write file to disc""" + if not os.access(os.path.dirname(filename) or ".", os.W_OK): + raise PyseederException("Can't write su3 file, access forbidden") + + nullbyte = bytes([0]) + with open(filename, "wb") as f: + f.write("I2Psu3".encode("utf-8")) + f.write(bytes([0,0])) + f.write(self.SIGNATURE_TYPE.to_bytes(2, "big")) + f.write(self.SIGNATURE_LENGTH.to_bytes(2, "big")) + f.write(nullbyte) + f.write(bytes([self.VERSION_LENGTH])) + f.write(nullbyte) + f.write(bytes([self.SIGNER_ID_LENGTH])) + f.write(self.CONTENT_LENGTH.to_bytes(8, "big")) + f.write(nullbyte) + f.write(bytes([self.FILE_TYPE])) + f.write(nullbyte) + f.write(bytes([self.CONTENT_TYPE])) + f.write(bytes([0 for _ in range(12)])) + f.write(self.VERSION + bytes( + [0 for _ in range(16 - len(self.VERSION))])) + f.write(self.SIGNER_ID.encode("utf-8")) + f.write(self.CONTENT) + + pyseeder.crypto.append_signature(filename, priv_key, priv_key_password) + + def reseed(self, netdb): + """Compress netdb entries and set content""" + zip_file = io.BytesIO() + dat_files = [] + + if not os.path.exists(netdb): + raise PyseederException("Wrong netDb path") + + if not os.access(netdb, os.R_OK): + raise PyseederException("Can't read netDb, access forbidden") + + for root, dirs, files in os.walk(netdb): + for f in files: + if f.endswith(".dat"): + # TODO check modified time + # may be not older than 10h + dat_files.append(os.path.join(root, f)) + + if len(dat_files) < 100: + raise PyseederException("Can't get enough netDb entries. Wrong netDb path?") + + with ZipFile(zip_file, "w", compression=ZIP_DEFLATED) as zf: + for f in random.sample(dat_files, 75): + zf.write(f, arcname=os.path.split(f)[1]) + + self.FILE_TYPE = 0x00 + self.CONTENT_TYPE = 0x03 + self.CONTENT = zip_file.getvalue() + self.CONTENT_LENGTH = len(self.CONTENT) diff --git a/pyseeder/transport.py b/pyseeder/transport.py new file mode 100644 index 0000000..76d1700 --- /dev/null +++ b/pyseeder/transport.py @@ -0,0 +1,58 @@ +"""Module for managing transport tasks""" +import urllib.request +from urllib.error import URLError +import os, os.path +import importlib + +from pyseeder.utils import PyseederException + + +RESEED_URLS = [ + "https://reseed.i2p-projekt.de/", + "https://i2p.mooo.com/netDb/", + "https://netdb.i2p2.no/", + "https://us.reseed.i2p2.no:444/", + "https://uk.reseed.i2p2.no:444/", + "https://i2p.manas.ca:8443/", + "https://i2p-0.manas.ca:8443/", + "https://reseed.i2p.vzaws.com:8443/", + "https://user.mx24.eu/", + "https://download.xxlspeed.com/", +] + +def download(url, filename): + """Download .su3 file, return True on success""" + USER_AGENT = "Wget/1.11.4" + + url = "{}i2pseeds.su3".format(url) + req = urllib.request.Request(url, headers={"User-Agent": USER_AGENT}) + + try: + with urllib.request.urlopen(req) as resp: + with open(filename, 'wb') as f: + f.write(resp.read()) + + if os.stat(filename).st_size > 0: + return True + else: + return False + except URLError as e: + return False + + +def upload(filename, config): + """Upload .su3 file with transports""" + if "transports" in config and "enabled" in config["transports"]: + for t in config["transports"]["enabled"].split(): + if t in config: + tconf = config[t] + else: + tconf = None + + try: + importlib.import_module("pyseeder.transports.{}".format(t)) \ + .run(filename, tconf) + except ImportError: + raise PyseederException( + "{} transport can't be loaded".format(t)) + diff --git a/pyseeder/transports/__init__.py b/pyseeder/transports/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pyseeder/transports/dropbox.py b/pyseeder/transports/dropbox.py new file mode 100644 index 0000000..9b6f9fc --- /dev/null +++ b/pyseeder/transports/dropbox.py @@ -0,0 +1,3 @@ + +def run(filename, config): + print("dummy dropbox plugin") diff --git a/pyseeder/transports/git.py b/pyseeder/transports/git.py new file mode 100644 index 0000000..0d9db6a --- /dev/null +++ b/pyseeder/transports/git.py @@ -0,0 +1,39 @@ +"""Git transport plugin""" +import subprocess +import os, os.path +from shutil import copyfile +from pyseeder.utils import TransportException + +TRANSPORT_NAME = "git" + +# Push to github repo witout prompting password. +# Set up SSH keys or change origin URL like that: +# git remote set-url origin https://$USERNAME:$PASSWORD@github.com/$USERNAME/$REPO.git + +def run(filename, config): + if "folder" not in config: + raise TransportException("git: No folder specified in config") + else: + REPO_FOLDER = config["folder"] + + REPO_FILE = os.path.split(filename)[1] + + if not os.access(REPO_FOLDER, os.W_OK): + raise TransportException("git: {} access forbidden" \ + .format(REPO_FOLDER)) + + if not os.path.isfile(filename): + raise TransportException("git: input file not found") + + copyfile(filename, os.path.join(REPO_FOLDER, REPO_FILE)) + + commands = [ + "git add {}".format(REPO_FILE), + "git commit -m 'update'", + "git push origin master" + ] + + cwd = os.getcwd() + os.chdir(REPO_FOLDER) + for c in commands: subprocess.call(c, shell=True) + os.chdir(cwd) diff --git a/pyseeder/utils.py b/pyseeder/utils.py new file mode 100644 index 0000000..3a56eaa --- /dev/null +++ b/pyseeder/utils.py @@ -0,0 +1,7 @@ +"""Various code""" + +class PyseederException(Exception): + pass + +class TransportException(PyseederException): + pass diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8ec0b10 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,6 @@ +cffi==1.7.0 +cryptography==1.4 +idna==2.1 +pyasn1==0.1.9 +pycparser==2.14 +six==1.10.0 diff --git a/transports.ini.example b/transports.ini.example new file mode 100644 index 0000000..285eaef --- /dev/null +++ b/transports.ini.example @@ -0,0 +1,10 @@ +[transports] +; enabled transports separated by space +enabled=git + +[git] +; Folder with git repository to use +folder=/home/user/reseed-data-repo + +[dropbox] +; todo