Critical: build_leaf_cert self-signed leaves with the leaf key instead of the YubiKey-held Intermediate CA key. Now extracts TBS, signs via pkcs11-tool (ECDSA-SHA384), reassembles, and verifies against the intermediate CA public key before returning. - setup-certauth.sh: ~1100 lines of stale inline api/ copies replaced with copy-from-repo (single source of truth); writes private /etc/certauth/certauth.env (0600); DB init loads env, no more swallowed errors; systemd unit gets EnvironmentFile= - config.py: YubiKey serials no longer hard-coded (env, fail closed); aarch64-only PKCS#11 path replaced with arch-neutral default; all paths env-overridable (CERTAUTH_*) - main.py: removed dead fastapi.security.CSRFProtection import (crashed startup); module-relative static/templates dirs; created_by resolved from the authenticated user instead of hard-coded 1; unclosed file handles fixed; domain_id 0 stored as NULL (FK bug) - models.py: certificates.domain_id FK pointed at users(id), now domains(id) - login: CSRF token now actually sent and validated - tests: 23 tests (auth, API flows, DER helpers, signing pipeline) - README, LICENSE, requirements.txt, pyproject.toml
585 lines
20 KiB
Python
585 lines
20 KiB
Python
import os
|
|
import secrets
|
|
import logging
|
|
import html
|
|
from datetime import datetime, timezone
|
|
from fastapi import FastAPI, Request, Depends, HTTPException, Form
|
|
from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse, PlainTextResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from pydantic import BaseModel
|
|
from jose import jwt
|
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
from config import (
|
|
ALGORITHM,
|
|
CA_CHAIN_PATH,
|
|
INT_CA_PATH,
|
|
ISSUED_DIR,
|
|
PFX_DEFAULT_PASSWORD,
|
|
ROOT_CA_PATH,
|
|
SECRET_KEY,
|
|
TMP_DIR,
|
|
)
|
|
from models import get_db, init_db, verify_password
|
|
from auth import create_access_token, get_current_user
|
|
from signing import build_leaf_cert
|
|
from cryptography.hazmat.primitives import serialization
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
STATIC_DIR = os.environ.get("CERTAUTH_STATIC_DIR", os.path.join(BASE_DIR, "static"))
|
|
TEMPLATES_DIR = os.environ.get("CERTAUTH_TEMPLATES_DIR", os.path.join(BASE_DIR, "templates"))
|
|
|
|
app = FastAPI(title="CertAuth Key Vault")
|
|
app.mount("/static", StaticFiles(directory=STATIC_DIR, check_dir=False), name="static")
|
|
|
|
_csrf_secrets = {}
|
|
|
|
def get_csrf_token(session_id: str) -> str:
|
|
if session_id not in _csrf_secrets:
|
|
_csrf_secrets[session_id] = secrets.token_hex(32)
|
|
return _csrf_secrets[session_id]
|
|
|
|
def verify_csrf_token(session_id: str, token: str) -> bool:
|
|
stored = _csrf_secrets.get(session_id)
|
|
if not stored:
|
|
return False
|
|
return secrets.compare_digest(stored, token)
|
|
|
|
def sanitize_error(msg: str) -> str:
|
|
return html.escape(str(msg))
|
|
|
|
def get_user_from_cookie(request: Request):
|
|
token = request.cookies.get("token")
|
|
if not token:
|
|
return None
|
|
try:
|
|
return jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
|
except Exception:
|
|
return None
|
|
|
|
jinja_env = Environment(
|
|
loader=FileSystemLoader(TEMPLATES_DIR),
|
|
autoescape=select_autoescape(["html", "xml"]),
|
|
)
|
|
|
|
@app.on_event("startup")
|
|
def startup():
|
|
init_db()
|
|
try:
|
|
with open(ROOT_CA_PATH) as f:
|
|
root = f.read()
|
|
with open(INT_CA_PATH) as f:
|
|
inter = f.read()
|
|
with open(CA_CHAIN_PATH, "w") as f:
|
|
f.write(inter + "\n" + root)
|
|
except Exception as e:
|
|
logger.warning("CA chain setup failed: %s", sanitize_error(str(e)))
|
|
|
|
def get_user_id(conn, username: str) -> int:
|
|
row = conn.execute("SELECT id FROM users WHERE username = ?", (username,)).fetchone()
|
|
return row["id"] if row else 1
|
|
|
|
|
|
def render(name, ctx):
|
|
return HTMLResponse(jinja_env.get_template(name).render(**ctx))
|
|
|
|
class LoginRequest(BaseModel):
|
|
username: str
|
|
password: str
|
|
|
|
@app.post("/api/token")
|
|
async def login(req: LoginRequest):
|
|
conn = get_db()
|
|
row = conn.execute("SELECT * FROM users WHERE username = ?", (req.username,)).fetchone()
|
|
conn.close()
|
|
if not row or not verify_password(req.password, row["password_hash"]):
|
|
raise HTTPException(401, "Invalid credentials")
|
|
token = create_access_token({"sub": req.username})
|
|
return {"access_token": token, "token_type": "bearer"}
|
|
|
|
@app.get("/api/me")
|
|
async def me(user: str = Depends(get_current_user)):
|
|
return {"username": user}
|
|
|
|
@app.get("/api/domains")
|
|
async def list_domains(user: str = Depends(get_current_user)):
|
|
conn = get_db()
|
|
rows = conn.execute("SELECT * FROM domains ORDER BY created_at DESC").fetchall()
|
|
conn.close()
|
|
return [dict(r) for r in rows]
|
|
|
|
@app.post("/api/domains")
|
|
async def create_domain(
|
|
name: str = Form(...),
|
|
description: str = Form(""),
|
|
user: str = Depends(get_current_user),
|
|
):
|
|
conn = get_db()
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"INSERT INTO domains (name, description, created_by) VALUES (?,?,?)",
|
|
(name, description, get_user_id(conn, user)),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
return {"status": "ok"}
|
|
|
|
@app.get("/api/certs")
|
|
async def list_certs(user: str = Depends(get_current_user)):
|
|
conn = get_db()
|
|
rows = conn.execute(
|
|
"SELECT c.*, d.name as domain_name FROM certificates c "
|
|
"LEFT JOIN domains d ON c.domain_id = d.id "
|
|
"ORDER BY c.created_at DESC"
|
|
).fetchall()
|
|
conn.close()
|
|
return [dict(r) for r in rows]
|
|
|
|
@app.post("/api/certs/request")
|
|
async def request_cert(
|
|
cn: str = Form(...),
|
|
sans: str = Form(""),
|
|
days: int = Form(365),
|
|
domain_id: int = Form(0),
|
|
user: str = Depends(get_current_user),
|
|
):
|
|
conn = get_db()
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"INSERT INTO certificates (domain_id, subject, san, status, created_by) VALUES (?,?,?,?,?)",
|
|
(domain_id or None, cn, sans, "pending", get_user_id(conn, user)),
|
|
)
|
|
conn.commit()
|
|
cid = cur.lastrowid
|
|
conn.close()
|
|
return {"status": "ok", "id": cid}
|
|
|
|
@app.post("/api/certs/{cert_id}/sign")
|
|
async def sign_cert(cert_id: int, user: str = Depends(get_current_user)):
|
|
conn = get_db()
|
|
row = conn.execute(
|
|
"SELECT * FROM certificates WHERE id = ?", (cert_id,)
|
|
).fetchone()
|
|
if not row or row["status"] != "pending":
|
|
conn.close()
|
|
raise HTTPException(400, "Not found or already signed")
|
|
conn.close()
|
|
try:
|
|
result, err = build_leaf_cert(row["subject"], row["san"], 365)
|
|
except Exception as e:
|
|
logger.error("Signing failed: %s", sanitize_error(str(e)))
|
|
raise HTTPException(500, "Signing failed")
|
|
if err:
|
|
raise HTTPException(500, "Signing failed")
|
|
cf = f"{ISSUED_DIR}/cert-{result['serial']}.crt"
|
|
kf = f"{ISSUED_DIR}/cert-{result['serial']}.key"
|
|
with open(cf, "w") as f:
|
|
f.write(result["cert_pem"])
|
|
with open(kf, "w") as f:
|
|
f.write(result["key_pem"])
|
|
os.chmod(cf, 0o640)
|
|
os.chmod(kf, 0o600)
|
|
conn = get_db()
|
|
conn.execute(
|
|
"UPDATE certificates SET status=?, serial=?, cert_path=?, issued_at=?, expires_at=? WHERE id=?",
|
|
("issued", result["serial"], cf, datetime.now(timezone.utc).isoformat(), result["expires_at"], cert_id),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
return {"status": "ok", "serial": result["serial"]}
|
|
|
|
@app.get("/api/certs/{cert_id}/pem")
|
|
async def download_pem(cert_id: int, request: Request = None):
|
|
user = get_user_from_cookie(request)
|
|
if not user:
|
|
raise HTTPException(401, "Login required")
|
|
conn = get_db()
|
|
row = conn.execute(
|
|
"SELECT * FROM certificates WHERE id = ?", (cert_id,)
|
|
).fetchone()
|
|
conn.close()
|
|
if not row or row["status"] != "issued":
|
|
raise HTTPException(404)
|
|
pem_path = f"{TMP_DIR}/cert-{row['serial']}.pem"
|
|
with open(row["cert_path"]) as f:
|
|
cert_pem = f.read()
|
|
with open(CA_CHAIN_PATH) as f:
|
|
chain_pem = f.read()
|
|
with open(pem_path, "w") as f:
|
|
f.write(cert_pem.rstrip() + "\n" + chain_pem)
|
|
return FileResponse(pem_path, media_type="application/x-pem-file", filename=f"cert-{row['serial']}.pem")
|
|
|
|
@app.get("/api/certs/{cert_id}/pfx")
|
|
async def download_pfx(
|
|
cert_id: int,
|
|
password: str = PFX_DEFAULT_PASSWORD,
|
|
request: Request = None,
|
|
):
|
|
user = get_user_from_cookie(request)
|
|
if not user:
|
|
raise HTTPException(401, "Login required")
|
|
conn = get_db()
|
|
row = conn.execute(
|
|
"SELECT * FROM certificates WHERE id = ?", (cert_id,)
|
|
).fetchone()
|
|
conn.close()
|
|
if not row or row["status"] != "issued":
|
|
raise HTTPException(404)
|
|
kf = row["cert_path"].replace(".crt", ".key")
|
|
if not os.path.exists(kf):
|
|
raise HTTPException(404)
|
|
from cryptography.hazmat.primitives.serialization import pkcs12, BestAvailableEncryption
|
|
from cryptography import x509
|
|
with open(row["cert_path"], "rb") as f:
|
|
leaf = x509.load_pem_x509_certificate(f.read())
|
|
with open(kf, "rb") as f:
|
|
key = serialization.load_pem_private_key(f.read(), password=None)
|
|
chain_certs = []
|
|
with open(CA_CHAIN_PATH, "rb") as f:
|
|
for cert_pem in f.read().split(b"-----END CERTIFICATE-----"):
|
|
cert_pem = cert_pem.strip()
|
|
if cert_pem:
|
|
chain_certs.append(
|
|
x509.load_pem_x509_certificate(
|
|
cert_pem + b"\n-----END CERTIFICATE-----"
|
|
)
|
|
)
|
|
pfx_data = pkcs12.serialize_key_and_certificates(
|
|
name=row["subject"].encode(),
|
|
key=key,
|
|
cert=leaf,
|
|
cas=chain_certs or None,
|
|
encryption_algorithm=BestAvailableEncryption(password.encode()),
|
|
)
|
|
pfx_path = f"{TMP_DIR}/cert-{row['serial']}.pfx"
|
|
with open(pfx_path, "wb") as f:
|
|
f.write(pfx_data)
|
|
return FileResponse(pfx_path, media_type="application/x-pkcs12", filename=f"cert-{row['serial']}.pfx")
|
|
|
|
@app.get("/api/health")
|
|
async def health():
|
|
return {"status": "ok"}
|
|
|
|
@app.get("/api/ca-chain")
|
|
async def ca_chain():
|
|
return FileResponse(CA_CHAIN_PATH, filename="ca-chain.crt")
|
|
|
|
@app.get("/", response_class=HTMLResponse)
|
|
async def dashboard(request: Request):
|
|
user = get_user_from_cookie(request)
|
|
if not user:
|
|
return RedirectResponse("/login", status_code=302)
|
|
conn = get_db()
|
|
certs = conn.execute(
|
|
"SELECT c.*, d.name as domain_name FROM certificates c "
|
|
"LEFT JOIN domains d ON c.domain_id = d.id "
|
|
"ORDER BY c.created_at DESC LIMIT 20"
|
|
).fetchall()
|
|
domains = conn.execute("SELECT * FROM domains").fetchall()
|
|
p = conn.execute(
|
|
"SELECT COUNT(*) as c FROM certificates WHERE status = ?",
|
|
("pending",),
|
|
).fetchone()["c"]
|
|
i = conn.execute(
|
|
"SELECT COUNT(*) as c FROM certificates WHERE status = ?",
|
|
("issued",),
|
|
).fetchone()["c"]
|
|
conn.close()
|
|
return render(
|
|
"dashboard.html",
|
|
{
|
|
"request": request,
|
|
"user": user,
|
|
"certs": [dict(r) for r in certs],
|
|
"domains": [dict(r) for r in domains],
|
|
"pending": p,
|
|
"issued": i,
|
|
"csrf_token": get_csrf_token(user.get("sub", "anon")),
|
|
},
|
|
)
|
|
|
|
@app.get("/login", response_class=HTMLResponse)
|
|
async def login_page(request: Request):
|
|
return render("login.html", {"request": request, "error": None, "csrf_token": get_csrf_token("anon")})
|
|
|
|
@app.post("/login")
|
|
async def login_post(
|
|
username: str = Form(...),
|
|
password: str = Form(...),
|
|
csrf_token: str = Form(""),
|
|
):
|
|
if not verify_csrf_token("anon", csrf_token):
|
|
return render(
|
|
"login.html",
|
|
{"request": None, "error": "Invalid request", "csrf_token": get_csrf_token("anon")},
|
|
)
|
|
conn = get_db()
|
|
row = conn.execute(
|
|
"SELECT * FROM users WHERE username = ?", (username,)
|
|
).fetchone()
|
|
conn.close()
|
|
if not row or not verify_password(password, row["password_hash"]):
|
|
return render(
|
|
"login.html",
|
|
{"request": None, "error": "Invalid credentials", "csrf_token": get_csrf_token("anon")},
|
|
)
|
|
token = create_access_token({"sub": username})
|
|
resp = RedirectResponse("/", status_code=302)
|
|
resp.set_cookie("token", token, httponly=True, samesite="lax", secure=True, path="/")
|
|
return resp
|
|
|
|
@app.post("/api/certs/{cert_id}/sign/web")
|
|
async def sign_cert_web(cert_id: int, request: Request = None):
|
|
user = get_user_from_cookie(request)
|
|
if not user:
|
|
return RedirectResponse("/login", status_code=302)
|
|
csrf = request.form.get("csrf_token", "")
|
|
if not verify_csrf_token(user.get("sub", "anon"), csrf):
|
|
return HTMLResponse("<span class='text-red-400'>Invalid request</span>", status_code=403)
|
|
conn = get_db()
|
|
row = conn.execute(
|
|
"SELECT * FROM certificates WHERE id = ?", (cert_id,)
|
|
).fetchone()
|
|
if not row or row["status"] != "pending":
|
|
conn.close()
|
|
return HTMLResponse("<span class='text-red-400'>Not found or already issued</span>", status_code=400)
|
|
conn.close()
|
|
try:
|
|
result, err = build_leaf_cert(row["subject"], row["san"], 365)
|
|
if err:
|
|
return HTMLResponse("<span class='text-red-400'>Issue failed</span>")
|
|
cf = f"{ISSUED_DIR}/cert-{result['serial']}.crt"
|
|
kf = f"{ISSUED_DIR}/cert-{result['serial']}.key"
|
|
with open(cf, "w") as f:
|
|
f.write(result["cert_pem"])
|
|
with open(kf, "w") as f:
|
|
f.write(result["key_pem"])
|
|
os.chmod(cf, 0o640)
|
|
os.chmod(kf, 0o600)
|
|
conn2 = get_db()
|
|
conn2.execute(
|
|
"UPDATE certificates SET status=?, serial=?, cert_path=?, issued_at=?, expires_at=? WHERE id=?",
|
|
(
|
|
"issued",
|
|
result["serial"],
|
|
cf,
|
|
datetime.now(timezone.utc).isoformat(),
|
|
result["expires_at"],
|
|
cert_id,
|
|
),
|
|
)
|
|
conn2.commit()
|
|
conn2.close()
|
|
return HTMLResponse(
|
|
f'<span class="text-green-400">Issued! '
|
|
f'<a href="/api/certs/{cert_id}/pem" class="underline">PEM</a> | '
|
|
f'<a href="/api/certs/{cert_id}/pfx" class="underline">PFX</a> | '
|
|
f'<a href="/certs" class="underline">Refresh</a></span>'
|
|
)
|
|
except Exception as ex:
|
|
logger.error("Signing failed: %s", sanitize_error(str(ex)))
|
|
return HTMLResponse("<span class='text-red-400'>Issue failed</span>")
|
|
|
|
@app.get("/logout")
|
|
async def logout():
|
|
resp = RedirectResponse("/login", status_code=302)
|
|
resp.delete_cookie("token", path="/")
|
|
return resp
|
|
|
|
@app.post("/api/domains/web")
|
|
async def create_domain_web(
|
|
name: str = Form(...),
|
|
description: str = Form(""),
|
|
csrf_token: str = Form(""),
|
|
request: Request = None,
|
|
):
|
|
user = get_user_from_cookie(request)
|
|
if not user:
|
|
return RedirectResponse("/login", status_code=302)
|
|
if not verify_csrf_token(user.get("sub", "anon"), csrf_token):
|
|
return HTMLResponse("<span class='text-red-400'>Invalid request</span>", status_code=403)
|
|
conn = get_db()
|
|
cur = conn.cursor()
|
|
cur.execute(
|
|
"INSERT INTO domains (name, description, created_by) VALUES (?,?,?)",
|
|
(name, description, get_user_id(conn, user.get("sub"))),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
return HTMLResponse(
|
|
'<span class="text-green-400">Domain registered! '
|
|
'<a href="/domains" class="underline">Refresh</a></span>'
|
|
)
|
|
|
|
@app.post("/api/certs/web/request")
|
|
async def request_cert_web(
|
|
cn: str = Form(...),
|
|
sans: str = Form(""),
|
|
days: int = Form(365),
|
|
domain_id: int = Form(0),
|
|
csrf_token: str = Form(""),
|
|
request: Request = None,
|
|
):
|
|
user = get_user_from_cookie(request)
|
|
if not user:
|
|
return RedirectResponse("/login", status_code=302)
|
|
if not verify_csrf_token(user.get("sub", "anon"), csrf_token):
|
|
return HTMLResponse("<span class='text-red-400'>Invalid request</span>", status_code=403)
|
|
conn = get_db()
|
|
cur = conn.cursor()
|
|
if domain_id == 0:
|
|
cur.execute("SELECT id FROM domains WHERE name=?", (cn,))
|
|
row = cur.fetchone()
|
|
domain_id = row[0] if row else None
|
|
cur.execute(
|
|
"INSERT INTO certificates (domain_id, subject, san, status, created_by) VALUES (?,?,?,?,?)",
|
|
(domain_id, cn, sans, "pending", get_user_id(conn, user.get("sub"))),
|
|
)
|
|
conn.commit()
|
|
conn.close()
|
|
return HTMLResponse(
|
|
'<span class="text-green-400">Certificate requested! Click Issue below. '
|
|
'<a href="/certs" class="underline">Refresh</a></span>'
|
|
)
|
|
|
|
@app.get("/domains", response_class=HTMLResponse)
|
|
async def domains_page(request: Request):
|
|
user = get_user_from_cookie(request)
|
|
if not user:
|
|
return RedirectResponse("/login", status_code=302)
|
|
conn = get_db()
|
|
rows = conn.execute("SELECT * FROM domains ORDER BY created_at DESC").fetchall()
|
|
conn.close()
|
|
return render(
|
|
"domains.html",
|
|
{
|
|
"request": request,
|
|
"user": user,
|
|
"domains": [dict(r) for r in rows],
|
|
"csrf_token": get_csrf_token(user.get("sub", "anon")),
|
|
},
|
|
)
|
|
|
|
@app.get("/certs", response_class=HTMLResponse)
|
|
async def certs_page(request: Request):
|
|
user = get_user_from_cookie(request)
|
|
if not user:
|
|
return RedirectResponse("/login", status_code=302)
|
|
conn = get_db()
|
|
rows = conn.execute(
|
|
"SELECT c.*, d.name as domain_name FROM certificates c "
|
|
"LEFT JOIN domains d ON c.domain_id = d.id "
|
|
"ORDER BY c.created_at DESC"
|
|
).fetchall()
|
|
domains = conn.execute("SELECT * FROM domains").fetchall()
|
|
conn.close()
|
|
return render(
|
|
"certs.html",
|
|
{
|
|
"request": request,
|
|
"user": user,
|
|
"certs": [dict(r) for r in rows],
|
|
"domains": [dict(r) for r in domains],
|
|
"csrf_token": get_csrf_token(user.get("sub", "anon")),
|
|
},
|
|
)
|
|
|
|
@app.get("/history", response_class=HTMLResponse)
|
|
async def history_page(request: Request):
|
|
user = get_user_from_cookie(request)
|
|
if not user:
|
|
return RedirectResponse("/login", status_code=302)
|
|
conn = get_db()
|
|
rows = conn.execute(
|
|
"SELECT c.*, d.name as domain_name FROM certificates c "
|
|
"LEFT JOIN domains d ON c.domain_id = d.id "
|
|
"ORDER BY c.created_at DESC"
|
|
).fetchall()
|
|
conn.close()
|
|
return render(
|
|
"history.html",
|
|
{
|
|
"request": request,
|
|
"user": user,
|
|
"certs": [dict(r) for r in rows],
|
|
},
|
|
)
|
|
|
|
@app.get("/setup", response_class=HTMLResponse)
|
|
async def setup_page(request: Request):
|
|
user = get_user_from_cookie(request)
|
|
if not user:
|
|
return RedirectResponse("/login", status_code=302)
|
|
return render(
|
|
"setup.html",
|
|
{
|
|
"request": request,
|
|
"user": user,
|
|
},
|
|
)
|
|
|
|
@app.get("/setup.sh")
|
|
async def setup_sh():
|
|
script = r'''#!/bin/bash
|
|
set -e
|
|
DETECTED_IP=""
|
|
if [[ -n "$1" ]]; then
|
|
DETECTED_IP="$1"
|
|
elif [[ -n "$CERTAUTH_IP" ]]; then
|
|
DETECTED_IP="$CERTAUTH_IP"
|
|
else
|
|
DETECTED_IP=$(hostname -I 2>/dev/null | awk '{print $1}') || true
|
|
[[ -z "$DETECTED_IP" ]] && DETECTED_IP=$(ip route get 1 2>/dev/null | awk '{print $7}' | head -1) || true
|
|
fi
|
|
if [[ -z "$DETECTED_IP" ]]; then
|
|
read -r -p "Enter CertAuth server IP: " DETECTED_IP
|
|
fi
|
|
CHAIN_URL="http://$DETECTED_IP/api/ca-chain"
|
|
echo "Downloading CA chain..."
|
|
curl -sLk "$CHAIN_URL" -o /tmp/ca-chain.crt || { echo "Failed"; exit 1; }
|
|
if [[ -f /etc/os-release ]]; then
|
|
. /etc/os-release
|
|
if [[ "$ID" == "debian" || "$ID" == "ubuntu" ]]; then
|
|
sudo cp /tmp/ca-chain.crt /usr/local/share/ca-certificates/certauth.crt
|
|
sudo update-ca-certificates
|
|
elif [[ "$ID" == "alpine" ]]; then
|
|
sudo cp /tmp/ca-chain.crt /usr/local/share/ca-certificates/certauth.crt
|
|
sudo update-ca-certificates
|
|
else
|
|
sudo cp /tmp/ca-chain.crt /etc/pki/ca-trust/source/anchors/certauth.crt
|
|
sudo update-ca-trust 2>/dev/null || true
|
|
fi
|
|
elif [[ "$(uname)" == "Darwin" ]]; then
|
|
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain /tmp/ca-chain.crt
|
|
else
|
|
echo "Unsupported OS"
|
|
exit 1
|
|
fi
|
|
rm -f /tmp/ca-chain.crt
|
|
echo "Done!"
|
|
'''
|
|
return PlainTextResponse(script, media_type="text/x-shellscript")
|
|
|
|
@app.get("/setup.ps1")
|
|
async def setup_ps1():
|
|
script = r'''
|
|
param([string]$CertAuthIP = "")
|
|
if (-not $CertAuthIP) {
|
|
$CertAuthIP = Read-Host "Enter CertAuth server IP"
|
|
}
|
|
$ChainUrl = "http://$CertAuthIP/api/ca-chain"
|
|
$ChainPath = "$env:TEMP\ca-chain.crt"
|
|
(New-Object Net.WebClient).DownloadFile($ChainUrl, $ChainPath)
|
|
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store(
|
|
[System.Security.Cryptography.X509Certificates.StoreName]::Root,
|
|
[System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine)
|
|
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
|
|
$cert = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2($ChainPath)
|
|
$store.Add($cert)
|
|
$store.Close()
|
|
Remove-Item $ChainPath -Force -ErrorAction SilentlyContinue
|
|
Write-Host "Done!" -ForegroundColor Green
|
|
'''
|
|
return PlainTextResponse(script, media_type="text/plain")
|