Make public key extraction idempotent
This commit is contained in:
@@ -36,7 +36,7 @@ from ..service import (
|
|||||||
renew_certificate,
|
renew_certificate,
|
||||||
resolve_signature_hash,
|
resolve_signature_hash,
|
||||||
revoke_certificate,
|
revoke_certificate,
|
||||||
save_key,
|
save_key_once,
|
||||||
search_text,
|
search_text,
|
||||||
serialize_certificate_detail,
|
serialize_certificate_detail,
|
||||||
serialize_certificate_summary,
|
serialize_certificate_summary,
|
||||||
@@ -592,14 +592,14 @@ def register(app: KayaApp, kca: "Kca") -> None:
|
|||||||
await send_error(ctx, 404, "certificate not found")
|
await send_error(ctx, 404, "certificate not found")
|
||||||
return
|
return
|
||||||
parsed = cert_core.parse_certificate(bytes(cert.cert_der))
|
parsed = cert_core.parse_certificate(bytes(cert.cert_der))
|
||||||
key = await save_key(
|
key, created = await save_key_once(
|
||||||
vault,
|
vault,
|
||||||
cert_core.certificate_public_key(parsed),
|
cert_core.certificate_public_key(parsed),
|
||||||
name=f"{cert.name} public key",
|
name=f"{cert.name} public key",
|
||||||
comment=f"Extracted from certificate {cert.name}",
|
comment=f"Extracted from certificate {cert.name}",
|
||||||
source="transformed",
|
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")
|
@app.POST("/api/vaults/${vault_id}/certificates/${cert_id}/transform/request")
|
||||||
@operation(summary="Create a request from a certificate", tags=["certificates"])
|
@operation(summary="Create a request from a certificate", tags=["certificates"])
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ from ..service import (
|
|||||||
parse_uuid,
|
parse_uuid,
|
||||||
resolve_signature_hash,
|
resolve_signature_hash,
|
||||||
save_key,
|
save_key,
|
||||||
|
save_key_once,
|
||||||
search_text,
|
search_text,
|
||||||
serialize_request_detail,
|
serialize_request_detail,
|
||||||
serialize_request_summary,
|
serialize_request_summary,
|
||||||
@@ -258,14 +259,14 @@ def register(app: KayaApp, kca: "Kca") -> None:
|
|||||||
await send_error(ctx, 404, "request not found")
|
await send_error(ctx, 404, "request not found")
|
||||||
return
|
return
|
||||||
public_key = req_core.csr_public_key(req_core.parse_csr(bytes(row.req_der)))
|
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,
|
vault,
|
||||||
public_key,
|
public_key,
|
||||||
name=f"{row.name} public key",
|
name=f"{row.name} public key",
|
||||||
comment=f"Extracted from request {row.name}",
|
comment=f"Extracted from request {row.name}",
|
||||||
source="transformed",
|
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")
|
@app.POST("/api/vaults/${vault_id}/requests/${request_id}/transform/template")
|
||||||
@operation(summary="Create a template from a request", tags=["requests"])
|
@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 uuid import UUID
|
||||||
|
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
|
from tortoise.exceptions import IntegrityError
|
||||||
from tortoise.expressions import Q
|
from tortoise.expressions import Q
|
||||||
|
|
||||||
from kca.core.algorithms import (
|
from kca.core.algorithms import (
|
||||||
@@ -577,6 +578,35 @@ async def save_key(
|
|||||||
return 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:
|
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:
|
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")
|
raise ValidationError("this key has no private part")
|
||||||
@@ -1544,6 +1574,7 @@ __all__ = [
|
|||||||
"renew_certificate",
|
"renew_certificate",
|
||||||
"revoke_certificate",
|
"revoke_certificate",
|
||||||
"save_key",
|
"save_key",
|
||||||
|
"save_key_once",
|
||||||
"serialize_certificate_detail",
|
"serialize_certificate_detail",
|
||||||
"serialize_certificate_summary",
|
"serialize_certificate_summary",
|
||||||
"serialize_crl_detail",
|
"serialize_crl_detail",
|
||||||
|
|||||||
@@ -177,6 +177,52 @@ class RouteExtraTests(unittest.TestCase):
|
|||||||
response = await client.get(f"/api/vaults/{vault_id}/requests/{request['id']}")
|
response = await client.get(f"/api/vaults/{vault_id}/requests/{request['id']}")
|
||||||
assert response.json()["signed"] is True
|
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_test
|
||||||
async def test_invalid_csr_signature_cannot_be_issued(self) -> None:
|
async def test_invalid_csr_signature_cannot_be_issued(self) -> None:
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
|
|||||||
Reference in New Issue
Block a user