Make public key extraction idempotent

This commit is contained in:
2026-10-26 10:55:24 +00:00
parent c1d8711cb4
commit 7523c0b8c4
4 changed files with 83 additions and 5 deletions
@@ -36,7 +36,7 @@ from ..service import (
renew_certificate,
resolve_signature_hash,
revoke_certificate,
save_key,
save_key_once,
search_text,
serialize_certificate_detail,
serialize_certificate_summary,
@@ -592,14 +592,14 @@ def register(app: KayaApp, kca: "Kca") -> None:
await send_error(ctx, 404, "certificate not found")
return
parsed = cert_core.parse_certificate(bytes(cert.cert_der))
key = await save_key(
key, created = await save_key_once(
vault,
cert_core.certificate_public_key(parsed),
name=f"{cert.name} public key",
comment=f"Extracted from certificate {cert.name}",
source="transformed",
)
await send_json(ctx, 201, {"id": str(key.id), "name": key.name})
await send_json(ctx, 201 if created else 200, {"id": str(key.id), "name": key.name})
@app.POST("/api/vaults/${vault_id}/certificates/${cert_id}/transform/request")
@operation(summary="Create a request from a certificate", tags=["certificates"])
@@ -27,6 +27,7 @@ from ..service import (
parse_uuid,
resolve_signature_hash,
save_key,
save_key_once,
search_text,
serialize_request_detail,
serialize_request_summary,
@@ -258,14 +259,14 @@ def register(app: KayaApp, kca: "Kca") -> None:
await send_error(ctx, 404, "request not found")
return
public_key = req_core.csr_public_key(req_core.parse_csr(bytes(row.req_der)))
key = await save_key(
key, created = await save_key_once(
vault,
public_key,
name=f"{row.name} public key",
comment=f"Extracted from request {row.name}",
source="transformed",
)
await send_json(ctx, 201, {"id": str(key.id), "name": key.name})
await send_json(ctx, 201 if created else 200, {"id": str(key.id), "name": key.name})
@app.POST("/api/vaults/${vault_id}/requests/${request_id}/transform/template")
@operation(summary="Create a template from a request", tags=["requests"])
@@ -9,6 +9,7 @@ from typing import Any, Dict, Iterable, List, Mapping, Optional, Sequence, Tuple
from uuid import UUID
from cryptography import x509
from tortoise.exceptions import IntegrityError
from tortoise.expressions import Q
from kca.core.algorithms import (
@@ -577,6 +578,35 @@ async def save_key(
return key
async def save_key_once(
vault: Vault,
key_obj: Any,
*,
name: str,
comment: str = "",
source: str = "transformed",
) -> Tuple[Key, bool]:
"""Return an existing key with the same public material, or save a new one.
Public-key extraction from a certificate or request is idempotent: when
the key already exists in the vault, return it instead of violating the
``(vault, public_key_sha256)`` unique constraint.
"""
info = key_core.key_info(key_obj)
existing = await Key.get_or_none(vault_id=vault.id, public_key_sha256=info.public_sha256)
if existing is not None:
return existing, False
try:
created = await save_key(vault, key_obj, name=name, comment=comment, source=source)
except IntegrityError:
# A concurrent request may have inserted the same key first.
existing = await Key.get_or_none(vault_id=vault.id, public_key_sha256=info.public_sha256)
if existing is None:
raise
return existing, False
return created, True
async def load_private_key(key: Key) -> Any:
if not key.has_private or key.private_der is None or key.private_nonce is None:
raise ValidationError("this key has no private part")
@@ -1544,6 +1574,7 @@ __all__ = [
"renew_certificate",
"revoke_certificate",
"save_key",
"save_key_once",
"serialize_certificate_detail",
"serialize_certificate_summary",
"serialize_crl_detail",
+46
View File
@@ -177,6 +177,52 @@ class RouteExtraTests(unittest.TestCase):
response = await client.get(f"/api/vaults/{vault_id}/requests/{request['id']}")
assert response.json()["signed"] is True
@async_test
async def test_public_key_extraction_is_idempotent(self) -> None:
async with api_client() as client:
with oidc_user("alice"):
vault_id = await _vault(client)
key = await _key(client, vault_id, "leaf key")
cert = await _cert(client, vault_id, key["id"], "Leaf")
# The certificate's key already exists in the vault.
response = await client.post(
f"/api/vaults/{vault_id}/certificates/{cert['id']}/transform/open-key"
)
assert response.status_code == 200, response.text
assert response.json()["id"] == key["id"]
response = await client.post(
f"/api/vaults/{vault_id}/requests",
json={
"name": "web request",
"subject": _subject("web.example.com"),
"new_key": {"key_type": "ec", "curve": "secp256r1", "name": "web key"},
},
)
assert response.status_code == 201, response.text
request = response.json()
response = await client.post(
f"/api/vaults/{vault_id}/requests/{request['id']}/transform/open-key"
)
assert response.status_code == 200, response.text
assert response.json()["id"] == request["key_id"]
# When the key is absent, extraction creates it once and then reuses it.
await client.delete(f"/api/vaults/{vault_id}/keys/{key['id']}")
response = await client.post(
f"/api/vaults/{vault_id}/certificates/{cert['id']}/transform/open-key"
)
assert response.status_code == 201, response.text
extracted = response.json()
response = await client.post(
f"/api/vaults/{vault_id}/certificates/{cert['id']}/transform/open-key"
)
assert response.status_code == 200, response.text
assert response.json()["id"] == extracted["id"]
keys = (await client.get(f"/api/vaults/{vault_id}/keys")).json()["results"]
assert len(keys) == 2
@async_test
async def test_invalid_csr_signature_cannot_be_issued(self) -> None:
from cryptography import x509