Spaces:
Sleeping
Sleeping
Download main.py from roseluo/WaveSign-API: direct link, hf CLI and curl.
- Browser
- Download file 6.08 kB
-
https://huggingface.co/spaces/roseluo/WaveSign-API/resolve/main/main.py
- Command line
-
hf download hf://spaces/roseluo/WaveSign-API/main.py
-
curl -L -o main.py https://huggingface.co/spaces/roseluo/WaveSign-API/resolve/main/main.py
6.08 kB
| """ | |
| WaveSign API — Private signing and verification service. | |
| """ | |
| import io | |
| import json | |
| import os | |
| import zipfile | |
| from fastapi import FastAPI, Form, Request, UploadFile | |
| from fastapi.responses import JSONResponse, StreamingResponse | |
| from core import embed_watermark, sign_image, verify_image, detect_mode | |
| from pdf_utils import sign_pdf, verify_pdf | |
| # --------------------------------------------------------------------------- | |
| # App setup | |
| # --------------------------------------------------------------------------- | |
| app = FastAPI(title="WaveSign API", docs_url=None, redoc_url=None) | |
| # API key loaded once at startup | |
| _API_KEY: str = os.environ.get("WS_API_KEY", "") | |
| # Routes that do not require authentication | |
| _PUBLIC_ROUTES = {"/health", "/"} | |
| # --------------------------------------------------------------------------- | |
| # Auth middleware | |
| # --------------------------------------------------------------------------- | |
| async def require_api_key(request: Request, call_next): | |
| if request.url.path in _PUBLIC_ROUTES: | |
| return await call_next(request) | |
| auth_header = request.headers.get("Authorization", "") | |
| if not auth_header.startswith("Bearer "): | |
| return JSONResponse( | |
| status_code=401, | |
| content={"detail": "Missing or malformed Authorization header."}, | |
| ) | |
| token = auth_header[len("Bearer "):] | |
| if not _API_KEY or token != _API_KEY: | |
| return JSONResponse( | |
| status_code=401, | |
| content={"detail": "Invalid API key."}, | |
| ) | |
| return await call_next(request) | |
| # --------------------------------------------------------------------------- | |
| # Helpers | |
| # --------------------------------------------------------------------------- | |
| def _is_pdf(filename: str) -> bool: | |
| return filename.lower().endswith(".pdf") | |
| def _build_zip(files: dict) -> io.BytesIO: | |
| """ | |
| Build an in-memory ZIP archive. | |
| files: {archive_name: bytes} | |
| """ | |
| buf = io.BytesIO() | |
| with zipfile.ZipFile(buf, mode="w", compression=zipfile.ZIP_DEFLATED) as zf: | |
| for name, data in files.items(): | |
| zf.writestr(name, data) | |
| buf.seek(0) | |
| return buf | |
| # --------------------------------------------------------------------------- | |
| # Endpoints | |
| # --------------------------------------------------------------------------- | |
| async def root(): | |
| return { | |
| "service": "WaveSign API", | |
| "version": "1.0", | |
| "endpoints": { | |
| "POST /sign": "Sign an image or PDF. Params: file, key. Returns ZIP.", | |
| "POST /verify": "Verify a signed file. Params: file, sig_file, key. Returns JSON.", | |
| "GET /health": "Service health check.", | |
| }, | |
| "auth": "Bearer token required for /sign and /verify.", | |
| } | |
| async def health(): | |
| return {"status": "ok", "service": "WaveSign API"} | |
| async def sign( | |
| file: UploadFile, | |
| key: str = Form(), | |
| ): | |
| try: | |
| raw = await file.read() | |
| filename = file.filename or "upload" | |
| if _is_pdf(filename): | |
| # PDF path | |
| signed_pdf_bytes, sigs, _ = sign_pdf(raw, secret=key) | |
| sig_json = json.dumps(sigs, indent=2).encode() | |
| archive = _build_zip({"signed.pdf": signed_pdf_bytes, "sig.json": sig_json}) | |
| else: | |
| # Image path | |
| from PIL import Image | |
| img = Image.open(io.BytesIO(raw)) | |
| mode = detect_mode(img) | |
| wm_img = embed_watermark(img, key, mode=mode) | |
| sig = sign_image(wm_img, key, mode=mode) | |
| out_buf = io.BytesIO() | |
| wm_img.save(out_buf, format="PNG") | |
| signed_png = out_buf.getvalue() | |
| sig_json = json.dumps(sig, indent=2).encode() | |
| archive = _build_zip({"signed.png": signed_png, "sig.json": sig_json}) | |
| return StreamingResponse( | |
| archive, | |
| media_type="application/zip", | |
| headers={"Content-Disposition": 'attachment; filename="wavesign_output.zip"'}, | |
| ) | |
| except Exception as exc: | |
| return JSONResponse(status_code=400, content={"detail": str(exc)}) | |
| async def verify( | |
| file: UploadFile, | |
| sig_file: UploadFile, | |
| key: str = Form(), | |
| ): | |
| try: | |
| raw = await file.read() | |
| sig_raw = await sig_file.read() | |
| filename = file.filename or "upload" | |
| sig_data = json.loads(sig_raw.decode()) | |
| if _is_pdf(filename): | |
| # sig_data is a list of per-page signature dicts | |
| per_page = verify_pdf(raw, secret=key, signatures=sig_data) | |
| overall = all(p.get("is_valid", False) for p in per_page) | |
| verdict = "AUTHENTIC" if overall else "TAMPERED or INVALID KEY" | |
| # Normalise per-page entries to use consistent field names | |
| normalised = [] | |
| for p in per_page: | |
| normalised.append({ | |
| "page_index": p.get("page_index"), | |
| "is_valid": p.get("is_valid", False), | |
| "verdict": "AUTHENTIC" if p.get("is_valid", False) else "TAMPERED or INVALID KEY", | |
| "similarity_score": p.get("similarity_score"), | |
| "mode": p.get("mode"), | |
| }) | |
| return JSONResponse({ | |
| "is_valid": overall, | |
| "verdict": verdict, | |
| "pages": normalised, | |
| }) | |
| else: | |
| # Image path — sig_data is a single signature dict | |
| from PIL import Image | |
| img = Image.open(io.BytesIO(raw)) | |
| result = verify_image(img, key, sig_data) | |
| is_valid = result.get("is_valid", False) | |
| return JSONResponse({ | |
| "is_valid": is_valid, | |
| "verdict": "AUTHENTIC" if is_valid else "TAMPERED or INVALID KEY", | |
| "similarity_score": result.get("similarity_score"), | |
| "mode": result.get("mode"), | |
| }) | |
| except Exception as exc: | |
| return JSONResponse(status_code=400, content={"detail": str(exc)}) | |