diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..dd0767d --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +max-line-length = 160 +exclude = docs/*, .git, __pycache__, build diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..510611e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,29 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v3.2.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files +- repo: https://github.com/asottile/reorder_python_imports + rev: v3.9.0 + hooks: + - id: reorder-python-imports + args: [--application-directories, '.:crowdtls', --py39-plus] +- repo: https://github.com/psf/black + rev: 23.3.0 + hooks: + - id: black + language_version: python3.11 +- repo: https://github.com/PyCQA/bandit + rev: 1.7.5 + hooks: + - id: bandit + args: ["-c", "pyproject.toml"] + additional_dependencies: ["bandit[toml]"] +- repo: https://github.com/pycqa/flake8 + rev: 6.0.0 + hooks: + - id: flake8 + exclude: ^migrations/versions/ diff --git a/README.md b/README.md index 4f4fe51..5025718 100644 --- a/README.md +++ b/README.md @@ -1 +1,30 @@ -# CrowdTLS-server \ No newline at end of file +
+CrowdTLS Logo + +# CrowdTLS-server + +CrowdTLS validates SSL/TLS certificates against the crowd. + +This is the backend server repository for it.
+ +[Installation](#installation) • +[License](#license) +
+ +## Installation + +TODO + +## FAQ + +**What is this? I'm looking for the browser extension!** + +You're in the wrong place. The browser extension can be found [here](https://sillyhats.mips.uk/pdf/CrowdTLS). + +## License + +This project is licensed under the MPL 2.0 License. See the `LICENSE` file for details. + +I carefully evaluated various open-source licenses and chose the Mozilla Public License 2.0 (MPL 2.0) for CrowdTLS due to its compatibility with other licenses, strong copyleft provisions, and its alignment with my values and goals. MPL 2.0 ensures that the source code remains open and available, while allowing for flexibility in terms of collaboration and incorporation into other projects. + +While I understand that different licenses may have their merits, I believe that MPL 2.0 provides the best balance of openness, collaborative potential, and legal clarity for the development and distribution of CrowdTLS. diff --git a/crowdtls.png b/crowdtls.png new file mode 100644 index 0000000..3f216f1 Binary files /dev/null and b/crowdtls.png differ diff --git a/crowdtls/__init__.py b/crowdtls/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/crowdtls/cli.py b/crowdtls/cli.py new file mode 100644 index 0000000..3016ee0 --- /dev/null +++ b/crowdtls/cli.py @@ -0,0 +1,50 @@ +from typing import Dict +from typing import List + +from fastapi import FastAPI +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.ext.asyncio import create_async_engine +from sqlalchemy.future import select +from sqlmodel import SQLModel + +from crowdtls.helpers import decode_der +from db import CertificateChain + +DATABASE_URL = "postgresql+asyncpg://user:password@localhost:5432/database" +engine = create_async_engine(DATABASE_URL, echo=True) + +app = FastAPI() + + +@app.on_event("startup") +async def startup_event(): + async with engine.begin() as connection: + await connection.run_sync(SQLModel.metadata.create_all) + + +@app.post("/check") +async def check_fingerprints(fingerprints: Dict[str, List[int]]): + fps = fingerprints.get("fps") + async with AsyncSession(engine) as session: + for fp in fps: + stmt = select(CertificateChain).where(CertificateChain.fingerprint == fp) + result = await session.execute(stmt) + certificate = result.scalars().first() + if not certificate: + return {"send": True} + return {"send": False} + + +@app.post("/new") +async def new_fingerprints(fingerprints: Dict[str, List[int]]): + async with AsyncSession(engine) as session: + for fp, _ in fingerprints.items(): + stmt = select(CertificateChain).where(CertificateChain.fingerprint == fp) + result = await session.execute(stmt) + certificate = result.scalars().first() + if not certificate: + new_certificate = decode_der(fp) + session.add(new_certificate) + pass + await session.commit() + return {"status": "OK"} diff --git a/crowdtls/db.py b/crowdtls/db.py new file mode 100644 index 0000000..e750f04 --- /dev/null +++ b/crowdtls/db.py @@ -0,0 +1,22 @@ +from sqlalchemy import Integer +from sqlalchemy import LargeBinary +from sqlalchemy.dialects.postgresql import JSONB +from sqlmodel import Field +from sqlmodel import SQLModel + + +class CertificateChain(SQLModel, table=True): + id: int = Field(default=None, primary_key=True) + fingerprint: str = Field(index=True) + domain_name: str + raw_der_certificate: LargeBinary + version: int + serial_number: str + signature: LargeBinary + issuer: JSONB + validity: JSONB + subject: JSONB + subject_public_key_info: JSONB + issuer_unique_id: Integer + subject_unique_id: Integer + extensions: JSONB diff --git a/crowdtls/helpers.py b/crowdtls/helpers.py new file mode 100644 index 0000000..c865f55 --- /dev/null +++ b/crowdtls/helpers.py @@ -0,0 +1,30 @@ +from typing import List + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend + +from crowdtls.db import CertificateChain + + +def decode_der(raw_der_certificate: List[int]) -> CertificateChain: + # Convert list of integers to bytes + der_cert_bytes = bytes(raw_der_certificate) + + # Parse the DER certificate + cert = x509.load_der_x509_certificate(der_cert_bytes, default_backend()) + + certificate_chain = CertificateChain( + raw_der_certificate=der_cert_bytes, + version=cert.version.value, + serial_number=cert.serial_number, + signature=cert.signature, + issuer=cert.issuer.rfc4514_string(), + validity={"not_valid_before": cert.not_valid_before, "not_valid_after": cert.not_valid_after}, + subject=cert.subject.rfc4514_string(), + subject_public_key_info=cert.public_key().public_bytes(), + issuer_unique_id=cert.issuer_unique_id, + subject_unique_id=cert.subject_unique_id, + extensions=[str(ext) for ext in cert.extensions], + ) + + return certificate_chain diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..b4fc738 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,37 @@ +[build-system] +requires = ["setuptools>=67.8"] +build-backend = "setuptools.build_meta" + +[project] +name = "crowdtls" +version = "0.0.1" +authors = [{ name = "crowdtls", email = "git@nixon.mozmail.com" }] +description = "Backend server for CrowdTLS browser extension" +readme = "README.md" +requires-python = ">=3.9" +license = { text = "MIT" } +dependencies = [ + "fastapi==0.95.2", + "uvicorn==0.22.0", + "loguru==0.7.0", + "pydantic==1.10.8", + "asyncpg==0.27.0", + "greenlet==2.0.2", + "sqlmodel==0.0.8", + "sqlalchemy==1.4.41", +] + +[project.urls] +homepage = "https://github.com/DarrylNixon/CrowdTLS" +repository = "https://github.com/DarrylNixon/CrowdTLS-server" + +[tool.setuptools] +py-modules = ["crowdtls"] + +[tool.bandit] +exclude_dirs = ["/doc", "/build"] +# TODO: Stop skipping B104 (binding on 0.0.0.0), is there a nice way to get a good docker bind address? +skips = ["B104"] + +[tool.black] +line-length = 120