- #8: Remove hardcoded credentials, require env vars (YK_ROOT_PIN, YK_INT_PIN, ADMIN_PASSWORD, JWT_SECRET) - #11: JWT secret now random via secrets.token_hex(32) if not set - #12: Admin password from env var, not hardcoded - #14: XSS prevention - sanitize error messages, html.escape - #15: CSRF tokens on all forms - #16: Cookie Secure flag added - #7: datetime.utcnow() → datetime.now(timezone.utc) - #3: Temp files in tempfile.mkdtemp, cleaned after use - #22: Private keys via cryptography library (NoEncryption for now) - #26: DER construction via cryptography library - #27: CRL table added for certificate revocation - #29: WAL autocheckpoint enabled - #30: Caddyfile already has TLS (no change needed) - #6: .gitignore for .password, *.pem, *.key
562 lines
19 KiB
Python
562 lines
19 KiB
Python
import os
|
|
import secrets
|
|
import hashlib
|
|
import subprocess
|
|
import json
|
|
import logging
|
|
import html
|
|
from datetime import datetime, timezone
|
|
from fastapi import FastAPI, Request, Depends, HTTPException, Form
|
|
from fastapi.responses import HTMLResponse, FileResponse, RedirectResponse, JSONResponse, PlainTextResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from fastapi.security import CSRFProtection
|
|
from pydantic import BaseModel
|
|
from jose import jwt
|
|
from jinja2 import Environment, FileSystemLoader, select_autoescape
|
|
from config import *
|
|
from models import get_db, init_db, hash_password, 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__)
|
|
|
|
app = FastAPI(title="CertAuth Key Vault")
|
|
app.mount("/static", StaticFiles(directory="/opt/certauth/api/static"), 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("/opt/certauth/api/templates"),
|
|
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 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, 1),
|
|
)
|
|
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, cn, sans, "pending", 1),
|
|
)
|
|
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"/etc/ssl/ca/issued/cert-{result['serial']}.crt"
|
|
kf = f"/etc/ssl/ca/issued/cert-{result['serial']}.key"
|
|
open(cf, "w").write(result["cert_pem"])
|
|
open(kf, "w").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"/var/lib/certauth/tmp/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 = "certauth",
|
|
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"/var/lib/certauth/tmp/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(""),
|
|
):
|
|
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(f"<span class='text-red-400'>Issue failed</span>")
|
|
cf = f"/etc/ssl/ca/issued/cert-{result['serial']}.crt"
|
|
kf = f"/etc/ssl/ca/issued/cert-{result['serial']}.key"
|
|
open(cf, "w").write(result["cert_pem"])
|
|
open(kf, "w").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, 1),
|
|
)
|
|
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", 1),
|
|
)
|
|
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")
|