aleph-im / aleph-sdk-python

Python SDK library for the Aleph.im network
MIT License
3 stars 5 forks source link

Feature: Choose a ssl certificate other than the default one #98

Closed Antonyjin closed 8 months ago

Antonyjin commented 8 months ago

I had the following problem:

ssl:True [SSLCertVerificationError: (1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate

when using the function: AuthenticatedAlephHttpClient and AlephHttpClient

I searched on the internet for a way to solve this problem, but all the commands/advice given didn't work.

So I thought it would be a good idea to give the user the option of specifying a specific SSL certificate if they wish.

This worked in my case and gave me the option of continuing to use the SDK provided by Aleph.

MHHukiewitz commented 8 months ago

What was the context of your error? Did you develop an app that somehow has custom SSL handling? Could you share your code?

hoh commented 8 months ago

In addition to the comments from @MHHukiewitz , how can we ensure that this change keeps working ? Do you know how to add unit tests to test this feature automatically ?

Antonyjin commented 8 months ago

I tried to develop a very simple Python application with Flask. My goal was to try out some of the features of the Python SDK. I don't think this application uses a custom SSL.

Here's the code I ran:

app.py

import asyncio
from flask import Flask, render_template, request
from templates import script

app = Flask(__name__)

@app.route("/")
def index():
    return render_template("index.html")

@app.route("/submit", methods=["POST"])
def submit():
    if request.method == "POST":
        asyncio.run(script.create_agg())
        return render_template("index.html")
    return render_template("/")

@app.route("/get_aggregate", methods=["GET"])
def get_aggregate():
    if request.method == "GET":
        asyncio.run(script.get_agg())
        return render_template("index.html")
    return render_template("/")

@app.route("/post_file", methods=["POST"])
def post_file():
    file = request.files["file"]
    content = file.read()
    if request.method == "POST" and content != None:
        asyncio.run(script.post_file(content))
        return render_template("index.html")
    return render_template("/")

@app.route("/post_program", methods=["POST"])
def post_program():
    code = request.files["code"]
    if request.method == "POST" and code != None:
        asyncio.run(script.post_program(code))
        return render_template("index.html")
    return render_template("/")

if __name__ == "__main__":
    app.run(debug=True)

scripts.py

from aleph_message.models import MessageType
from aleph.sdk.chains.ethereum import get_fallback_account
from aleph.sdk.client import AuthenticatedAlephHttpClient
from aleph.sdk.client import AlephHttpClient
from aleph.sdk.types import StorageEnum
from aleph.sdk.conf import settings as sdk_settings
from aleph_message.models.execution.program import Encoding
import ssl
import certifi

sslcontext = ssl.create_default_context(cafile=certifi.where())

async def create_agg():
        account = get_fallback_account()
        async with AuthenticatedAlephHttpClient(account, ssl_context=sslcontext) as client:
            message, status = await client.create_aggregate(
                "profile",
                {"bio": "tester", "name": "Antony"},
            )
            print(message.content)
            print(message.item_hash)
            print(status)

async def get_agg():
    async with AlephHttpClient(ssl_context=sslcontext) as client:
        aggregate = await client.fetch_aggregate(
            "0xbC80BeEcBd67549E70cb1C729e903818E6370D37",
            "profile",
        )
        print(aggregate)

async def post_file(file_content):
    account = get_fallback_account()
    async with AuthenticatedAlephHttpClient(account, ssl_context=sslcontext) as client:
        storage = StorageEnum.ipfs if len(file_content) > 4 * 1024 * 1024 else StorageEnum.storage
        message, status = await client.create_store(
            account.get_address(),
            file_content,
            storage_engine=storage,
        )
        print(message.item_hash)
        print(status)

async def post_program(file):
    account = get_fallback_account()

    async with AuthenticatedAlephHttpClient(account, ssl_context=sslcontext) as client:
        file_content = file.read()
        storage_engine = StorageEnum.ipfs if len(file_content) > 4 * 1024 * 1024 else StorageEnum.storage
        user_code, status = await client.create_store(
            file_content=file_content,
            storage_engine=storage_engine,
            guess_mime_type=True,
        )

        print(status)
        program_ref = user_code.item_hash
        message, status = await client.create_program(
            program_ref=program_ref,
            entrypoint="main:app",
            encoding=Encoding.squashfs,
            runtime=sdk_settings.DEFAULT_RUNTIME_ID,
            channel="default",
        )
        print(f"Program created successfully with item hash: {message.item_hash}")
        print(status)
Antonyjin commented 8 months ago

I tested it on my app and it worked fine. But I understand that I shouldn't rely on that alone. I'll find out so I can provide unit tests with it, if the change is relevant and accepted of course.

hoh commented 8 months ago

The core issue is that apparently some systems running macOS have an issue providing CA Certificates to Python libraries. @Antonyjin reported that one of his friend could reproduce the problem while another could not.

Investigating the root cause of this issue may prove to be very time consuming, and @Antonyjin wanted to make something work within a reasonable timeframe. A solution was to use rely instead on the certifi library to provide CA Certificates authored by Mozilla.

In order for aiohttp to use a different set of CA, a custom version of the field ssl_context must be passed in argument.

Antonyjin commented 8 months ago

Okay, I'll take your comment into account and see how I can improve the code in this way. Do you want me to run some tests with the corrected version you provided and run the PR again once I'm sure it works, or would you like me to take your comment into account and run the PR again later?

MHHukiewitz commented 8 months ago

Should look good, running CI/CD and if green, we merge :+1:

MHHukiewitz commented 8 months ago

@Antonyjin best to run flake8 src/ tests/ && mypy src / tests/ && isort src/ tests/ && black src/ tests/ in order to fix all the code quality problems

Antonyjin commented 8 months ago

Okay working on it!