Make public key extraction idempotent
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user