mirror of https://github.com/PurpleI2P/pyseeder
libre-net-society
8 years ago
commit
03af5634c1
15 changed files with 491 additions and 0 deletions
@ -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. |
@ -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() |
@ -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) |
@ -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) |
@ -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)) |
||||||
|
|
@ -0,0 +1,3 @@ |
|||||||
|
|
||||||
|
def run(filename, config): |
||||||
|
print("dummy dropbox plugin") |
@ -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) |
@ -0,0 +1,7 @@ |
|||||||
|
"""Various code""" |
||||||
|
|
||||||
|
class PyseederException(Exception): |
||||||
|
pass |
||||||
|
|
||||||
|
class TransportException(PyseederException): |
||||||
|
pass |
@ -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 |
Loading…
Reference in new issue