from __future__ import annotations from pathlib import Path from typing import Protocol, cast import httpx import pytest from backup_tool.api.app import create_app from backup_tool.config import Settings from backup_tool.db.models import Repository from backup_tool.repository import initialize from cryptography.hazmat.primitives import serialization from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey PASSWORD = "a secure password" class SignedRepository(Protocol): repository_id: str signing_key_id: str signing_public_key: str def settings_for(tmp_path: Path) -> Settings: key = tmp_path / "master.key" key.write_bytes(b"m6-test-master-key-material-32-bytes-minimum") key.chmod(0o600) repositories = tmp_path / "repositories" repositories.mkdir() return Settings( data_dir=tmp_path, database_url=f"sqlite+aiosqlite:///{tmp_path / 'metadata.db'}", repository_roots=(repositories,), local_source_roots=(tmp_path,), restore_roots=(tmp_path,), master_key_file=key, min_free_bytes=1, ) def test_repository_initialization_creates_a_bound_ed25519_keypair( tmp_path: Path, ) -> None: settings = settings_for(tmp_path) initialized = cast(SignedRepository, initialize(settings, "main", "none", "none")) key_path = settings.data_dir / "repository-keys" / f"{initialized.repository_id}.ed25519" assert key_path.read_bytes() assert key_path.stat().st_mode & 0o777 == 0o600 private_key = Ed25519PrivateKey.from_private_bytes(key_path.read_bytes()) public_key = private_key.public_key().public_bytes( serialization.Encoding.Raw, serialization.PublicFormat.Raw, ) assert initialized.signing_public_key == public_key.hex() assert initialized.signing_key_id.startswith("ed25519-") @pytest.mark.asyncio async def test_repository_api_persists_its_bound_public_signing_key( tmp_path: Path, ) -> None: settings = settings_for(tmp_path) app = create_app(settings) from backup_tool.db.models import Base async with app.state.engine.begin() as connection: await connection.run_sync(Base.metadata.create_all) async with httpx.AsyncClient( transport=httpx.ASGITransport(app=app), base_url="https://test" ) as client: assert ( await client.post("/api/v2/setup", json={"username": "admin", "password": PASSWORD}) ).status_code == 201 created = await client.post( "/api/v2/repositories", json={ "name": "main", "relative_path": "main", "compression": "none", "encryption": "none", }, headers={"X-CSRF-Token": client.cookies["backup_tool_csrf"]}, ) assert created.status_code == 201 async with app.state.sessions() as db: repository = await db.get(Repository, created.json()["id"]) await app.state.engine.dispose() assert repository is not None assert repository.signing_key_id.startswith("ed25519-") assert len(repository.signing_public_key) == 64