WaveSign-API / main.py
roseluo's picture
Upload 2 files
068b4ac verified
Raw History Blame Contribute Delete
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
# ---------------------------------------------------------------------------
@app.middleware("http")
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
# ---------------------------------------------------------------------------
@app.get("/")
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.",
}
@app.get("/health")
async def health():
return {"status": "ok", "service": "WaveSign API"}
@app.post("/sign")
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)})
@app.post("/verify")
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)})