Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic updater #78

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open

Conversation

cvbnxx
Copy link
Contributor

@cvbnxx cvbnxx commented Apr 27, 2024

Works by creating a new Python script which downloads the latest release and replaces the user's old version.

The idea of creating a new script is to get around the permission errors caused when trying to do the download_and_replace function inside the original program, as it would be attempting to overwrite itself while open. Creating a new script circumvents this. The new script deletes itself after running.

Limitation: only replaces the old version if it is named exactly 'sgplus.exe'. (The new version will still be installed if the old version is called something else)

Not included: Option to enable/disable in the config. Auto update feature makes printed installation instructions redundant but they are not removed.

@ElectricityMachine ElectricityMachine linked an issue Apr 28, 2024 that may be closed by this pull request
@ElectricityMachine
Copy link
Owner

Your implementation is pretty interesting, good job! I do have one reservation though, which I forgot to include in the original issue:

A human won't be going to github.com and looking at the repository for the file. Having some sort of machine verification would make me feel better about automatically downloading things to peoples' computers.

Given that, I'd like to explore some verification methods out of curiosity. Signing with my PGP key (I already do this for commits) or having a signed SHA256SUMS file that the auto updater checks against seem like easy solutions. This would add some overhead, but Python has a builtin for hashing and for PGP, I see some packages we could use on PyPi. The downside of rolling our own verification solution is more bloat for a potentially unnecessary purpose. A code signing certificate would be an easy way out, but I don't have the money for one, nor do I want to deal with the headache of Microsoft's signing system.

I'll explore the threat models this would protect against and see how easy it is to incorporate PGP signing and SHA256SUMS verification. Let me know your thoughts on verification, especially if you feel like it's completely unnecessary :P

@cvbnxx
Copy link
Contributor Author

cvbnxx commented Apr 28, 2024

Having some kind of verification sounds like a good idea for sure.

I feel like the PGP way might be slightly easier and offer a bit more security (as far as I'm aware, the SHA256 method would only really verify data integrity and wouldn't offer much against malicious attacks though I could be wrong on that).

Either way some sort of verification should probably be added since all it does is download and run whatever file is considered the latest release without much care for what that actually is.

@cvbnxx
Copy link
Contributor Author

cvbnxx commented May 16, 2024

Sorry it took a while to start on this but I have something that sort-of works.

I originally started trying to use PGP modules but the more I looked into it, it seems that the end-user would need to have compatible software on their PC and I couldn't figure out a way around it.

Instead of using PGP itself I used the cryptography module to generate an RSA public-private key pairing and then used it to sign the file and generate a .sig file.

The auto-updater script that is generated by update_checker, then also uses the cryptography module to verify the signed file.
Right now it only works reliably with the signed file, signature, and public key placed in the same directory manually as I just can't get the public key to download properly.
Also it may actually work with PGP if you generate using RSA but I haven't tested this.
Placing the files in the directory manually, it seems to give the expected result but I've only lightly tested it.

#auto-updater.py
#This should go in the text field in generate_update_script in update_checker.py

import os
import hashlib
import shutil
import subprocess
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
from requests import get as requests_get

def download_file(url, filename):
    try:
        r = requests_get(url=url, stream=True)
        with open(filename, 'wb') as f:
            shutil.copyfileobj(r.raw, f)
    except Exception as e:
        print(f"Download failed: {{e}}")

def verify_signature(public_key_path, file_path, signature_path):
    with open(public_key_path, 'rb') as file:
        public_key = serialization.load_pem_public_key(file.read(), backend=default_backend())

    with open(file_path, 'rb') as file:
        data = file.read()

    with open(signature_path, 'rb') as file:
        signature = file.read()

    try:
        verifier = public_key.verify(data=data, signature=signature, padding=padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.MAX_LENGTH), algorithm=hashes.SHA256())
        return True
    except:
        return False

def update_executable(binary_url, public_key_path, signature_path):
    download_file(binary_url, "sgplus-temp.exe")
    download_file(public_key_path, "public_key.pem")
    download_file(signature_path, "signature.sig")
    
    if verify_signature("public_key.pem", "sgplus-temp.exe", "signature.sig"):
        print("Signature verification successful. Updating.")
        try:
            os.remove("sgplus.exe")
            os.rename("sgplus-temp.exe", "sgplus.exe")
            subprocess.Popen("sgplus.exe", creationflags = subprocess.CREATE_NEW_PROCESS_GROUP)
        except:
            print("Error replacing old file. This occurs if the original file was not named 'sgplus.exe'. Your updated file is 'sgplus-temp.exe'")
    else:
        print("Signature verification failed. Aborting update.")
        os.remove("sgplus-temp.exe")
     
download_url = "{url}"
public_key_path = "path/to/maintainer/public/key.pem"
signature_path = "{passed_to_generate_update_script}"

update_executable(download_url, public_key_path, signature_path)

os.remove("public_key.pem")
os.remove("signature.sig")
#os.remove(__file__)

The scripts for generating the keys and signing the file. I'm not an expert when it comes to cryptography and though I haven't encountered any specific issues there could be something I missed.

#generate_keys.py

from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import serialization

def generate_key_pair():
    private_key = rsa.generate_private_key(
        public_exponent=65537,
        key_size=2048,
        backend=default_backend()
    )
    public_key = private_key.public_key()
    return private_key, public_key

private_key, public_key = generate_key_pair()
with open('private_key.pem', 'wb') as f:
    f.write(private_key.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.TraditionalOpenSSL,
        encryption_algorithm=serialization.NoEncryption()
    ))

with open('public_key.pem', 'wb') as f:
    f.write(public_key.public_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PublicFormat.SubjectPublicKeyInfo
    ))

print("Done.")

and

#sign_file.py

import os
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.primitives import hashes

def sign_file(private_key_path, file_path, signature_path):
    with open(private_key_path, 'rb') as file:
        private_key = serialization.load_pem_private_key(
            file.read(),
            password=None,
        )

    with open(file_path, 'rb') as file:
        data = file.read()

    # Sign the data using the private key
    signature = private_key.sign(
        data,  # Data to be signed
        padding.PSS(
            mgf=padding.MGF1(hashes.SHA256()),
            salt_length=padding.PSS.MAX_LENGTH
        ),
        hashes.SHA256()
    )

    # Write the signature to a file
    with open(signature_path, 'wb') as file:
        file.write(signature)

private_key_path = 'private_key.pem'
file_path = 'sgplus.exe'
signature_path = 'signature.sig'
sign_file(private_key_path, file_path, signature_path)
print("Done")

@ElectricityMachine
Copy link
Owner

Great stuff! Will take a look when I have time. On my initial glance, it seems sound, and not requiring whoever downloads it to install GnuPG is a bonus. It's too bad Python doesn't have PGP in the standard library.

And there's no need to apologize - open source projects are inherently time-unlimited (as you can tell by my lack of activity on merging the other PRs or pushing commits because life gets in the way).

Another option would be to generate a signed SHA256SUMS file, and consider an exe that matches that hash as "signed." That way, people who don't like the auto-updater can verify manually with PGP, or if they don't care about that, just the hash. But I don't know if that would be better or worse - just different. In either case, I'll research a decent way to do signing on my machine, or ideally, via GH actions, to make any of this work. Cryptography is fun!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add auto updater
2 participants