Close XCA parity gaps: advanced extensions, CA/revocation UI, imports, ZIP export, E2E
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
RUN_IN_TRANSACTION = True
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "certificate" ADD "no_expiration" BOOL NOT NULL DEFAULT False;"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "certificate" DROP COLUMN "no_expiration";"""
|
||||
|
||||
|
||||
MODELS_STATE = (
|
||||
"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"
|
||||
"ndP8Y6VR4uZJNahv/wcN6vu0"
|
||||
)
|
||||
@@ -0,0 +1,86 @@
|
||||
from tortoise import BaseDBAsyncClient
|
||||
|
||||
RUN_IN_TRANSACTION = True
|
||||
|
||||
|
||||
async def upgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "authority" ADD "template_key" VARCHAR(64);"""
|
||||
|
||||
|
||||
async def downgrade(db: BaseDBAsyncClient) -> str:
|
||||
return """
|
||||
ALTER TABLE "authority" DROP COLUMN "template_key";"""
|
||||
|
||||
|
||||
MODELS_STATE = (
|
||||
"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"
|
||||
"CyviyPmclMn+6ZzmHyuNEjdPqkF9+z+XiHCY"
|
||||
)
|
||||
@@ -152,6 +152,9 @@ class Certificate(Model):
|
||||
issuer_hash = fields.CharField(max_length=64, db_index=True)
|
||||
not_before = fields.DatetimeField()
|
||||
not_after = fields.DatetimeField()
|
||||
# True when the certificate intentionally has no well-defined expiry
|
||||
# (the ASN.1 notAfter is a far-future sentinel, XCA's "no expiration").
|
||||
no_expiration = fields.BooleanField(default=False)
|
||||
is_ca = fields.BooleanField(default=False, db_index=True)
|
||||
path_length = fields.IntField(null=True)
|
||||
cert_der = fields.BinaryField()
|
||||
@@ -185,6 +188,9 @@ class Authority(Model):
|
||||
template: fields.ForeignKeyRelation["Template"] = fields.ForeignKeyField(
|
||||
"models.Template", related_name="authorities", on_delete=fields.SET_NULL, null=True
|
||||
)
|
||||
# A built-in template key (e.g. "builtin:ca"); mutually exclusive with
|
||||
# the user-template FK above.
|
||||
template_key = fields.CharField(max_length=64, null=True)
|
||||
crl_expire = fields.DatetimeField(null=True)
|
||||
crl_number = fields.IntField(default=0)
|
||||
crl_days = fields.IntField(default=30)
|
||||
|
||||
@@ -28,6 +28,7 @@ from ..service import (
|
||||
generate_crl,
|
||||
issue_certificate,
|
||||
load_private_key,
|
||||
preview_certificate,
|
||||
parse_datetime,
|
||||
parse_uuid,
|
||||
renew_certificate,
|
||||
@@ -96,6 +97,25 @@ def register(app: KayaApp, kca: "Kca") -> None:
|
||||
return
|
||||
await send_json(ctx, 201, await serialize_certificate_detail(cert, vault))
|
||||
|
||||
@app.POST("/api/vaults/${vault_id}/certificates/preview")
|
||||
@operation(summary="Preview/validate a certificate payload", tags=["certificates"])
|
||||
@require_auth(kca.oidc)
|
||||
async def preview(ctx: HttpContext, vault_id: str) -> None:
|
||||
vault = await _vault(kca, ctx, vault_id)
|
||||
if vault is None:
|
||||
return
|
||||
try:
|
||||
body = await read_json(ctx)
|
||||
except JsonRequestError as exc:
|
||||
await send_error(ctx, 400, str(exc))
|
||||
return
|
||||
try:
|
||||
result = await preview_certificate(vault, body)
|
||||
except (ValidationError, ValueError) as exc:
|
||||
await send_error(ctx, 400, str(exc))
|
||||
return
|
||||
await send_json(ctx, 200, result)
|
||||
|
||||
@app.GET("/api/vaults/${vault_id}/certificates/${cert_id}")
|
||||
@operation(summary="Certificate detail", tags=["certificates"])
|
||||
@require_auth(kca.oidc)
|
||||
@@ -273,7 +293,7 @@ def register(app: KayaApp, kca: "Kca") -> None:
|
||||
"crl_days": authority.crl_days,
|
||||
"crl_number": authority.crl_number,
|
||||
"crl_expire": authority.crl_expire.isoformat() if authority.crl_expire else None,
|
||||
"default_template_id": str(authority.template_id) if authority.template_id else None,
|
||||
"default_template": authority.template_key or (str(authority.template_id) if authority.template_id else None),
|
||||
"has_tls_auth_key": bool(authority.tls_auth_key),
|
||||
})
|
||||
|
||||
@@ -309,20 +329,30 @@ def register(app: KayaApp, kca: "Kca") -> None:
|
||||
if "template_id" in body:
|
||||
template_id = body.get("template_id")
|
||||
if template_id:
|
||||
template = await Template.get_or_none(id=parse_uuid(str(template_id)), vault_id=vault.id)
|
||||
if template is None:
|
||||
await send_error(ctx, 400, "template not found")
|
||||
return
|
||||
authority.template_id = template.id
|
||||
text = str(template_id)
|
||||
if text.startswith("builtin:"):
|
||||
if text not in {item.id for item in template_core.builtin_templates()}:
|
||||
await send_error(ctx, 400, "unknown built-in template")
|
||||
return
|
||||
authority.template_key = text
|
||||
authority.template_id = None
|
||||
else:
|
||||
template = await Template.get_or_none(id=parse_uuid(text), vault_id=vault.id)
|
||||
if template is None:
|
||||
await send_error(ctx, 400, "template not found")
|
||||
return
|
||||
authority.template_id = template.id
|
||||
authority.template_key = None
|
||||
else:
|
||||
authority.template_id = None
|
||||
authority.template_key = None
|
||||
await authority.save()
|
||||
await send_json(ctx, 200, {
|
||||
"certificate_id": str(cert.id),
|
||||
"crl_days": authority.crl_days,
|
||||
"crl_number": authority.crl_number,
|
||||
"crl_expire": authority.crl_expire.isoformat() if authority.crl_expire else None,
|
||||
"default_template_id": str(authority.template_id) if authority.template_id else None,
|
||||
"default_template": authority.template_key or (str(authority.template_id) if authority.template_id else None),
|
||||
"has_tls_auth_key": bool(authority.tls_auth_key),
|
||||
})
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
"""Vaults (XCA databases) and their settings."""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import io
|
||||
import json
|
||||
import zipfile
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional
|
||||
|
||||
@@ -15,6 +19,7 @@ from ..service import (
|
||||
DEFAULT_VAULT_SETTINGS,
|
||||
ValidationError,
|
||||
iso,
|
||||
load_private_key,
|
||||
merge_settings,
|
||||
parse_uuid,
|
||||
serialize_certificate_summary,
|
||||
@@ -27,11 +32,13 @@ from ..service import (
|
||||
vault_for_owner,
|
||||
vault_settings,
|
||||
)
|
||||
from ..core import certificates as cert_core
|
||||
from ..core import crls as crl_core
|
||||
from ..core import templates as template_core
|
||||
from ..core import extensions as ext_core
|
||||
from ..core import keys as key_core
|
||||
from ..core import names as name_core
|
||||
from ..core import requests as req_core
|
||||
from ..core import templates as template_core
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ..mixin import Kca
|
||||
@@ -219,6 +226,86 @@ def register(app: KayaApp, kca: "Kca") -> None:
|
||||
"recent_keys": [await serialize_key_summary(k) for k in recent_keys],
|
||||
})
|
||||
|
||||
@app.POST("/api/vaults/${vault_id}/export")
|
||||
@operation(summary="Export the whole vault as a ZIP", tags=["vaults"])
|
||||
@require_auth(kca.oidc)
|
||||
async def export_vault(ctx: HttpContext, vault_id: str) -> None:
|
||||
vault = await _vault_or_error(kca, ctx, vault_id)
|
||||
if vault is None:
|
||||
return
|
||||
try:
|
||||
body = await read_json(ctx)
|
||||
except JsonRequestError as exc:
|
||||
await send_error(ctx, 400, str(exc))
|
||||
return
|
||||
password = body.get("password")
|
||||
if password is not None and not isinstance(password, str):
|
||||
await send_error(ctx, 400, "password must be a string")
|
||||
return
|
||||
used: set[str] = set()
|
||||
|
||||
def safe(name: str, suffix: str) -> str:
|
||||
base = "".join(ch if ch.isalnum() or ch in "-_." else "_" for ch in name).strip("._") or "item"
|
||||
candidate = f"{base}{suffix}"
|
||||
index = 2
|
||||
while candidate.lower() in used:
|
||||
candidate = f"{base}_{index}{suffix}"
|
||||
index += 1
|
||||
used.add(candidate.lower())
|
||||
return candidate
|
||||
|
||||
buffer = io.BytesIO()
|
||||
manifest = {
|
||||
"format": "kca-vault",
|
||||
"version": 1,
|
||||
"exported_at": datetime.now(timezone.utc).isoformat(),
|
||||
"vault": {"id": str(vault.id), "name": vault.name, "description": vault.description},
|
||||
"items": [],
|
||||
"warnings": [],
|
||||
"private_keys_encrypted": bool(password),
|
||||
}
|
||||
with zipfile.ZipFile(buffer, "w", zipfile.ZIP_DEFLATED) as archive:
|
||||
for key in await Key.filter(vault_id=vault.id).order_by("name"):
|
||||
public = key_core.public_key_from_der(bytes(key.public_der))
|
||||
public_name = safe(key.name, ".pub")
|
||||
archive.writestr(f"keys/{public_name}", key_core.public_pem(public))
|
||||
if key.has_private and password:
|
||||
try:
|
||||
private = await load_private_key(key)
|
||||
private_name = safe(key.name, ".key")
|
||||
archive.writestr(f"keys/{private_name}", key_core.private_pem(private, password=password))
|
||||
except Exception: # noqa: BLE001 - keep the rest of the dump
|
||||
manifest["warnings"].append(f"could not export private key {key.name!r}")
|
||||
manifest["items"].append({"kind": "key", "id": str(key.id), "name": key.name, "has_private": key.has_private})
|
||||
for cert in await Certificate.filter(vault_id=vault.id).order_by("name"):
|
||||
parsed = cert_core.parse_certificate(bytes(cert.cert_der))
|
||||
archive.writestr(f"certificates/{safe(cert.name, '.crt')}", cert_core.certificate_pem(parsed))
|
||||
manifest["items"].append({"kind": "certificate", "id": str(cert.id), "name": cert.name, "subject": parsed.subject.rfc4514_string()})
|
||||
for request in await Request.filter(vault_id=vault.id).order_by("name"):
|
||||
parsed = req_core.parse_csr(bytes(request.req_der))
|
||||
archive.writestr(f"requests/{safe(request.name, '.csr')}", req_core.csr_pem(parsed))
|
||||
manifest["items"].append({"kind": "request", "id": str(request.id), "name": request.name})
|
||||
for crl in await Crl.filter(vault_id=vault.id).order_by("name"):
|
||||
parsed = crl_core.parse_crl(bytes(crl.crl_der))
|
||||
archive.writestr(f"crls/{safe(crl.name, '.crl')}", crl_core.crl_pem(parsed))
|
||||
manifest["items"].append({"kind": "crl", "id": str(crl.id), "name": crl.name})
|
||||
for template in await Template.filter(vault_id=vault.id).order_by("name"):
|
||||
data = template_core.template_to_xca_data({
|
||||
"name": template.name,
|
||||
"comment": template.comment,
|
||||
"subject": template.subject,
|
||||
"settings": template.settings,
|
||||
})
|
||||
archive.writestr(f"templates/{safe(template.name, '.xca')}", data)
|
||||
manifest["items"].append({"kind": "template", "id": str(template.id), "name": template.name})
|
||||
archive.writestr("manifest.json", json.dumps(manifest, indent=2))
|
||||
body_bytes = buffer.getvalue()
|
||||
await send_json(ctx, 200, {
|
||||
"filename": f"{safe(vault.name, '.zip')}",
|
||||
"content_type": "application/zip",
|
||||
"content_base64": base64.b64encode(body_bytes).decode("ascii"),
|
||||
})
|
||||
|
||||
@app.GET("/api/vaults/${vault_id}/options")
|
||||
@operation(summary="Reference data for editors", tags=["vaults"])
|
||||
@require_auth(kca.oidc)
|
||||
|
||||
@@ -193,6 +193,7 @@ async def serialize_certificate_summary(cert: Certificate) -> Dict[str, Any]:
|
||||
"serial_hex": cert.serial_hex,
|
||||
"not_before": iso(cert.not_before),
|
||||
"not_after": iso(cert.not_after),
|
||||
"no_expiration": cert.no_expiration,
|
||||
"is_ca": cert.is_ca,
|
||||
"path_length": cert.path_length,
|
||||
"has_private_key": bool(cert.key_id and await Key.filter(id=cert.key_id, has_private=True).exists()),
|
||||
@@ -270,7 +271,10 @@ async def serialize_certificate_detail(cert: Certificate, vault: Vault) -> Dict[
|
||||
"crl_expire": iso(authority.crl_expire) if authority else None,
|
||||
"crl_number": authority.crl_number if authority else None,
|
||||
"crl_days": authority.crl_days if authority else None,
|
||||
"default_template_id": str(authority.template_id) if authority and authority.template_id else None,
|
||||
"default_template": (
|
||||
authority.template_key
|
||||
or (str(authority.template_id) if authority.template_id else None)
|
||||
),
|
||||
"has_tls_auth_key": bool(authority and authority.tls_auth_key),
|
||||
}
|
||||
if authority
|
||||
@@ -560,14 +564,16 @@ def parse_extensions(payload: Any) -> Dict[str, Any]:
|
||||
return payload
|
||||
|
||||
|
||||
def validate_validity(payload: Mapping[str, Any]) -> Tuple[datetime, datetime]:
|
||||
def validate_validity(payload: Mapping[str, Any]) -> Tuple[datetime, datetime, bool]:
|
||||
now = datetime.now(timezone.utc)
|
||||
not_before = parse_datetime(payload.get("not_before") or now.isoformat(), "not_before")
|
||||
raw_after = payload.get("not_after")
|
||||
if raw_after:
|
||||
not_after = parse_datetime(raw_after, "not_after")
|
||||
validity = payload.get("validity") or {}
|
||||
no_expiration = bool(validity.get("no_expiration"))
|
||||
if no_expiration:
|
||||
not_after = cert_core.FAR_FUTURE
|
||||
elif payload.get("not_after"):
|
||||
not_after = parse_datetime(payload["not_after"], "not_after")
|
||||
else:
|
||||
validity = payload.get("validity") or {}
|
||||
amount = int(validity.get("amount", 365))
|
||||
unit = str(validity.get("unit", "days"))
|
||||
if unit == "days":
|
||||
@@ -578,12 +584,12 @@ def validate_validity(payload: Mapping[str, Any]) -> Tuple[datetime, datetime]:
|
||||
not_after = not_before + timedelta(days=amount * 365)
|
||||
else:
|
||||
raise ValidationError("validity unit must be days, months or years")
|
||||
if validity.get("midnight"):
|
||||
not_before = not_before.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
not_after = not_after.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
if validity.get("midnight") and not no_expiration:
|
||||
not_before = not_before.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
not_after = not_after.replace(hour=23, minute=59, second=59, microsecond=0)
|
||||
if not_after <= not_before:
|
||||
raise ValidationError("not_after must be later than not_before")
|
||||
return not_before, not_after
|
||||
return not_before, not_after, no_expiration
|
||||
|
||||
|
||||
async def unique_serial(vault: Vault, issuer: Optional[Certificate], bits: int) -> int:
|
||||
@@ -621,6 +627,20 @@ async def find_request(vault: Vault, request_id: Optional[str]) -> Optional[Requ
|
||||
return await Request.get_or_none(id=uuid, vault_id=vault.id)
|
||||
|
||||
|
||||
def check_mandatory_dn(subject: x509.Name, settings_obj: Mapping[str, Any]) -> None:
|
||||
mandatory = settings_obj.get("mandatory_dn") or []
|
||||
missing = []
|
||||
for name in mandatory:
|
||||
try:
|
||||
oid = name_core.resolve_oid(str(name))
|
||||
except ValueError:
|
||||
continue
|
||||
if not subject.get_attributes_for_oid(x509.oid.ObjectIdentifier(oid)):
|
||||
missing.append(str(name))
|
||||
if missing:
|
||||
raise ValidationError("missing mandatory subject entries: " + ", ".join(missing))
|
||||
|
||||
|
||||
async def issue_certificate(
|
||||
vault: Vault,
|
||||
payload: Mapping[str, Any],
|
||||
@@ -692,8 +712,9 @@ async def issue_certificate(
|
||||
if request_obj is not None and payload.get("copy_request_extensions"):
|
||||
extensions = ext_core.extension_specs(req_core.parse_csr(bytes(request_obj.req_der)).extensions)
|
||||
|
||||
not_before, not_after = validate_validity(payload)
|
||||
not_before, not_after, no_expiration = validate_validity(payload)
|
||||
settings_obj = vault_settings(vault)
|
||||
check_mandatory_dn(subject, settings_obj)
|
||||
bits = int(settings_obj.get("serial_len_bits", 64))
|
||||
raw_serial = payload.get("serial") or payload.get("serial_hex") or 0
|
||||
try:
|
||||
@@ -748,6 +769,7 @@ async def issue_certificate(
|
||||
issuer_hash=info.issuer_hash,
|
||||
not_before=info.not_before,
|
||||
not_after=info.not_after,
|
||||
no_expiration=no_expiration,
|
||||
is_ca=info.is_ca,
|
||||
path_length=info.path_length,
|
||||
cert_der=cert_core.certificate_der(cert),
|
||||
@@ -784,6 +806,67 @@ async def delete_certificate_cascade(vault: Vault, cert: Certificate) -> None:
|
||||
await cert.delete()
|
||||
|
||||
|
||||
async def preview_certificate(vault: Vault, payload: Mapping[str, Any]) -> Dict[str, Any]:
|
||||
"""Build the extension list for a wizard payload without persisting it."""
|
||||
request_obj = await find_request(vault, payload.get("request_id"))
|
||||
if payload.get("request_id") and request_obj is None:
|
||||
raise ValidationError("signing request not found")
|
||||
signer = await find_certificate(vault, payload.get("signer_id"))
|
||||
if payload.get("signer_id") and signer is None:
|
||||
raise ValidationError("signing certificate not found")
|
||||
if signer is not None and not signer.is_ca:
|
||||
raise ValidationError("the signing certificate is not a CA")
|
||||
|
||||
if request_obj is not None and not payload.get("modify_subject"):
|
||||
subject = req_core.parse_csr(bytes(request_obj.req_der)).subject
|
||||
else:
|
||||
subject = parse_subject(payload.get("subject") or [])
|
||||
if not subject.rdns:
|
||||
raise ValidationError("the subject must not be empty")
|
||||
check_mandatory_dn(subject, vault_settings(vault))
|
||||
|
||||
public_key = None
|
||||
key_model = await find_key(vault, payload.get("key_id"))
|
||||
if key_model is not None:
|
||||
public_key = key_core.public_key_from_der(bytes(key_model.public_der))
|
||||
elif request_obj is not None:
|
||||
public_key = req_core.csr_public_key(req_core.parse_csr(bytes(request_obj.req_der)))
|
||||
else:
|
||||
new_key = payload.get("new_key")
|
||||
if isinstance(new_key, Mapping):
|
||||
generated = key_core.generate_key(
|
||||
str(new_key.get("key_type") or "ec"),
|
||||
int(new_key.get("size") or 2048),
|
||||
new_key.get("curve") or "secp256r1",
|
||||
)
|
||||
else:
|
||||
generated = key_core.generate_key("ec", curve="secp256r1")
|
||||
public_key = generated.public_key()
|
||||
|
||||
extensions_spec = parse_extensions(payload.get("extensions"))
|
||||
if request_obj is not None and payload.get("copy_request_extensions"):
|
||||
extensions_spec = ext_core.extension_specs(req_core.parse_csr(bytes(request_obj.req_der)).extensions)
|
||||
issuer_cert = cert_core.parse_certificate(bytes(signer.cert_der)) if signer is not None else None
|
||||
try:
|
||||
extensions = ext_core.build_extensions(
|
||||
extensions_spec,
|
||||
public_key=public_key,
|
||||
subject_name=subject,
|
||||
issuer_cert=issuer_cert,
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise ValidationError(str(exc)) from exc
|
||||
not_before, not_after, no_expiration = validate_validity(payload)
|
||||
return {
|
||||
"subject": [entry.as_dict() for entry in name_core.entries_from_name(subject)],
|
||||
"extensions": ext_core.extension_rows(extensions),
|
||||
"openssl_config": ext_core.extension_config_lines(extensions),
|
||||
"not_before": iso(not_before),
|
||||
"not_after": iso(not_after),
|
||||
"no_expiration": no_expiration,
|
||||
}
|
||||
|
||||
|
||||
async def renew_certificate(
|
||||
vault: Vault,
|
||||
cert: Certificate,
|
||||
@@ -796,7 +879,8 @@ async def renew_certificate(
|
||||
parsed = cert_core.parse_certificate(bytes(cert.cert_der))
|
||||
keep_serial = bool(payload.get("keep_serial"))
|
||||
serial = parsed.serial_number if keep_serial else await unique_serial(vault, signer, int(vault_settings(vault).get("serial_len_bits", 64)))
|
||||
not_before, not_after = validate_validity(payload)
|
||||
not_before, not_after, no_expiration = validate_validity(payload)
|
||||
check_mandatory_dn(parsed.subject, vault_settings(vault))
|
||||
signer_private = await load_private_key(signer_key)
|
||||
signer_cert = cert_core.parse_certificate(bytes(signer.cert_der))
|
||||
new_cert = cert_core.build_certificate(
|
||||
@@ -825,6 +909,7 @@ async def renew_certificate(
|
||||
issuer_hash=info.issuer_hash,
|
||||
not_before=info.not_before,
|
||||
not_after=info.not_after,
|
||||
no_expiration=no_expiration,
|
||||
is_ca=info.is_ca,
|
||||
path_length=info.path_length,
|
||||
cert_der=cert_core.certificate_der(new_cert),
|
||||
@@ -1252,6 +1337,7 @@ __all__ = [
|
||||
"load_private_key",
|
||||
"merge_settings",
|
||||
"parse_datetime",
|
||||
"preview_certificate",
|
||||
"parse_key_generation",
|
||||
"parse_subject",
|
||||
"parse_uuid",
|
||||
|
||||
@@ -193,3 +193,116 @@ class RouteExtraTests(unittest.TestCase):
|
||||
assert response.status_code == 204
|
||||
response = await client.get(f"/api/vaults/{vault_id}/certificates/{cert['id']}")
|
||||
assert response.json()["key_id"] is None
|
||||
|
||||
|
||||
class ExtendedRouteTests(unittest.TestCase):
|
||||
@async_test
|
||||
async def test_preview_advanced_extensions(self) -> None:
|
||||
async with api_client() as client:
|
||||
with oidc_user("alice"):
|
||||
vault_id = await _vault(client)
|
||||
key = await _key(client, vault_id)
|
||||
payload = {
|
||||
"name": "preview",
|
||||
"key_id": key["id"],
|
||||
"subject": _subject("preview"),
|
||||
"extensions": {
|
||||
"basic_constraints": {"critical": True, "ca": False, "path_length": None},
|
||||
"crl_distribution_points": {"critical": False, "urls": ["http://example.com/ca.crl"]},
|
||||
"authority_information_access": {
|
||||
"critical": False,
|
||||
"descriptions": [{"method": "ocsp", "location": {"type": "uri", "value": "http://ocsp.example.com"}}],
|
||||
},
|
||||
"raw": [{"oid": "1.2.3.4", "critical": False, "der_hex": "0c03616263"}],
|
||||
},
|
||||
}
|
||||
response = await client.post(f"/api/vaults/{vault_id}/certificates/preview", json=payload)
|
||||
assert response.status_code == 200, response.text
|
||||
names = [row["name"] for row in response.json()["extensions"]]
|
||||
assert "CRL Distribution Points" in names
|
||||
assert "Authority Information Access" in names
|
||||
|
||||
@async_test
|
||||
async def test_mandatory_dn_and_no_expiration(self) -> None:
|
||||
async with api_client() as client:
|
||||
with oidc_user("alice"):
|
||||
vault_id = await _vault(client)
|
||||
settings = (await client.get(f"/api/vaults/{vault_id}/settings")).json()
|
||||
await client.patch(
|
||||
f"/api/vaults/{vault_id}/settings",
|
||||
json={"settings": {"mandatory_dn": ["CN"]}, "version": settings["version"]},
|
||||
)
|
||||
key = await _key(client, vault_id)
|
||||
now = datetime.now(timezone.utc)
|
||||
response = await client.post(
|
||||
f"/api/vaults/{vault_id}/certificates",
|
||||
json={
|
||||
"name": "missing cn",
|
||||
"key_id": key["id"],
|
||||
"subject": [{"name": "O", "value": "KCA"}],
|
||||
"validity": {"amount": 1, "unit": "days"},
|
||||
},
|
||||
)
|
||||
assert response.status_code == 400
|
||||
assert "mandatory" in response.json()["error"]
|
||||
response = await client.post(
|
||||
f"/api/vaults/{vault_id}/certificates",
|
||||
json={
|
||||
"name": "forever",
|
||||
"key_id": key["id"],
|
||||
"subject": _subject("forever"),
|
||||
"validity": {"no_expiration": True},
|
||||
},
|
||||
)
|
||||
assert response.status_code == 201, response.text
|
||||
assert response.json()["no_expiration"] is True
|
||||
|
||||
@async_test
|
||||
async def test_ca_template_and_revocation_edit(self) -> None:
|
||||
async with api_client() as client:
|
||||
with oidc_user("alice"):
|
||||
vault_id = await _vault(client)
|
||||
ca_key = await _key(client, vault_id, "ca")
|
||||
ca = await _cert(client, vault_id, ca_key["id"], "CA", ca=True)
|
||||
response = await client.patch(
|
||||
f"/api/vaults/{vault_id}/certificates/{ca['id']}/ca",
|
||||
json={"crl_days": 45, "template_id": "builtin:tls-server"},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["default_template"] == "builtin:tls-server"
|
||||
assert response.json()["crl_days"] == 45
|
||||
response = await client.post(
|
||||
f"/api/vaults/{vault_id}/certificates/{ca['id']}/revocations",
|
||||
json={"serial_hex": "1234", "reason": "keyCompromise", "invalidity_date": datetime.now(timezone.utc).isoformat()},
|
||||
)
|
||||
assert response.status_code == 201, response.text
|
||||
rev = response.json()
|
||||
response = await client.patch(
|
||||
f"/api/vaults/{vault_id}/certificates/{ca['id']}/revocations/{rev['id']}",
|
||||
json={"reason": "superseded", "invalidity_date": None},
|
||||
)
|
||||
assert response.status_code == 200, response.text
|
||||
assert response.json()["reason"] == "superseded"
|
||||
|
||||
|
||||
class VaultExportTests(unittest.TestCase):
|
||||
@async_test
|
||||
async def test_vault_zip_export(self) -> None:
|
||||
import io
|
||||
import zipfile
|
||||
async with api_client() as client:
|
||||
with oidc_user("alice"):
|
||||
vault_id = await _vault(client)
|
||||
key = await _key(client, vault_id, "zip key")
|
||||
cert = await _cert(client, vault_id, key["id"], "Zip Cert", ca=True)
|
||||
response = await client.post(f"/api/vaults/{vault_id}/export", json={"password": "secret"})
|
||||
assert response.status_code == 200, response.text
|
||||
payload = response.json()
|
||||
data = base64.b64decode(payload["content_base64"])
|
||||
with zipfile.ZipFile(io.BytesIO(data)) as archive:
|
||||
names = archive.namelist()
|
||||
assert "manifest.json" in names
|
||||
assert any(name.startswith("certificates/") for name in names)
|
||||
assert any(name.startswith("keys/") for name in names)
|
||||
manifest = archive.read("manifest.json")
|
||||
assert b"Zip Cert" in manifest
|
||||
|
||||
Reference in New Issue
Block a user