Add full XCA curve and digest parity
This commit is contained in:
@@ -34,6 +34,7 @@ from ..service import (
|
||||
parse_datetime,
|
||||
parse_uuid,
|
||||
renew_certificate,
|
||||
resolve_signature_hash,
|
||||
revoke_certificate,
|
||||
save_key,
|
||||
search_text,
|
||||
@@ -593,7 +594,7 @@ def register(app: KayaApp, kca: "Kca") -> None:
|
||||
parsed = cert_core.parse_certificate(bytes(cert.cert_der))
|
||||
key = await save_key(
|
||||
vault,
|
||||
parsed.public_key(),
|
||||
cert_core.certificate_public_key(parsed),
|
||||
name=f"{cert.name} public key",
|
||||
comment=f"Extracted from certificate {cert.name}",
|
||||
source="transformed",
|
||||
@@ -617,11 +618,17 @@ def register(app: KayaApp, kca: "Kca") -> None:
|
||||
return
|
||||
parsed = cert_core.parse_certificate(bytes(cert.cert_der))
|
||||
try:
|
||||
private_key = await load_private_key(key)
|
||||
signature_hash = resolve_signature_hash(
|
||||
private_key,
|
||||
None,
|
||||
cert_core.signature_hash_name(parsed) or "sha256",
|
||||
)
|
||||
csr = req_core.build_csr(
|
||||
subject=parsed.subject,
|
||||
private_key=await load_private_key(key),
|
||||
private_key=private_key,
|
||||
extension_spec=cert_core.certificate_extension_specs(parsed),
|
||||
signature_hash=cert_core.signature_hash_name(parsed),
|
||||
signature_hash=signature_hash,
|
||||
)
|
||||
except ValueError as exc:
|
||||
await send_error(ctx, 400, str(exc))
|
||||
|
||||
@@ -22,8 +22,10 @@ from ..service import (
|
||||
ValidationError,
|
||||
generate_key,
|
||||
parse_extensions,
|
||||
parse_key_generation,
|
||||
parse_subject,
|
||||
parse_uuid,
|
||||
resolve_signature_hash,
|
||||
save_key,
|
||||
search_text,
|
||||
serialize_request_detail,
|
||||
@@ -31,6 +33,7 @@ from ..service import (
|
||||
serialize_template_summary,
|
||||
load_private_key,
|
||||
vault_for_owner,
|
||||
vault_settings,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -96,26 +99,40 @@ def register(app: KayaApp, kca: "Kca") -> None:
|
||||
key_obj = await load_private_key(key_model)
|
||||
elif isinstance(body.get("new_key"), dict):
|
||||
new_key = body["new_key"]
|
||||
generated = await generate_key(
|
||||
str(new_key.get("key_type") or "rsa"),
|
||||
int(new_key.get("size") or key_core.DEFAULT_KEY_SIZE),
|
||||
new_key.get("curve"),
|
||||
params = parse_key_generation({
|
||||
"key_type": new_key.get("key_type") or "rsa",
|
||||
"size": new_key.get("size", key_core.DEFAULT_KEY_SIZE),
|
||||
"curve": new_key.get("curve"),
|
||||
"name": new_key.get("name") or name,
|
||||
})
|
||||
generated = await generate_key(params["key_type"], params["size"], params["curve"])
|
||||
# Reject an invalid explicit/default digest before leaving a
|
||||
# newly generated key persisted without a corresponding CSR.
|
||||
resolve_signature_hash(
|
||||
generated,
|
||||
body.get("signature_hash"),
|
||||
vault_settings(vault).get("default_hash", "sha256"),
|
||||
)
|
||||
key_model = await save_key(
|
||||
vault,
|
||||
generated,
|
||||
name=str(new_key.get("name") or name),
|
||||
name=params["name"],
|
||||
comment="Generated while creating a request",
|
||||
source="generated",
|
||||
)
|
||||
key_obj = generated
|
||||
else:
|
||||
raise ValidationError("select an existing key or generate a new one")
|
||||
signature_hash = resolve_signature_hash(
|
||||
key_obj,
|
||||
body.get("signature_hash"),
|
||||
vault_settings(vault).get("default_hash", "sha256"),
|
||||
)
|
||||
csr = req_core.build_csr(
|
||||
subject=subject,
|
||||
private_key=key_obj,
|
||||
extension_spec=extensions,
|
||||
signature_hash=body.get("signature_hash"),
|
||||
signature_hash=signature_hash,
|
||||
attributes=body.get("attributes") or {},
|
||||
)
|
||||
except (ValidationError, ValueError) as exc:
|
||||
|
||||
@@ -319,8 +319,10 @@ def register(app: KayaApp, kca: "Kca") -> None:
|
||||
return
|
||||
await send_json(ctx, 200, {
|
||||
"key_types": key_core.key_type_options(),
|
||||
"hash_algorithms": list(key_core.HASH_ALGORITHMS),
|
||||
"curves": list(key_core.EC_CURVES),
|
||||
"hash_algorithms": [item.name for item in key_core.HASHES],
|
||||
"hash_options": key_core.hash_options(),
|
||||
"curves": [curve.name for curve in key_core.CURVES],
|
||||
"curve_options": key_core.curve_options(),
|
||||
"dn_attributes": name_core.name_attribute_options(),
|
||||
"eku": name_core.eku_options(),
|
||||
"key_usages": ext_core.KEY_USAGES,
|
||||
|
||||
@@ -11,6 +11,12 @@ from uuid import UUID
|
||||
from cryptography import x509
|
||||
from tortoise.expressions import Q
|
||||
|
||||
from kca.core.algorithms import (
|
||||
HASH_BY_NAME,
|
||||
canonical_curve_name,
|
||||
default_signature_hash,
|
||||
validate_signature_hash,
|
||||
)
|
||||
from kca.core import certificates as cert_core
|
||||
from kca.core import crls as crl_core
|
||||
from kca.core import extensions as ext_core
|
||||
@@ -66,8 +72,24 @@ def merge_settings(current: Mapping[str, Any], patch: Mapping[str, Any]) -> Dict
|
||||
raise ValidationError(f"unknown setting {key!r}")
|
||||
merged[key] = value
|
||||
# Validate the values that affect cryptographic operations.
|
||||
if merged.get("default_hash") not in key_core.HASH_ALGORITHMS:
|
||||
raise ValidationError("default_hash must be sha1, sha224, sha256, sha384 or sha512")
|
||||
if merged.get("default_hash") not in HASH_BY_NAME:
|
||||
raise ValidationError("default_hash must be one of: " + ", ".join(HASH_BY_NAME))
|
||||
default_key = merged.get("default_key") or {}
|
||||
if not isinstance(default_key, Mapping):
|
||||
raise ValidationError("default_key must be an object")
|
||||
try:
|
||||
parsed_default_key = parse_key_generation({
|
||||
"key_type": default_key.get("type") or "rsa",
|
||||
"size": default_key.get("size", key_core.DEFAULT_KEY_SIZE),
|
||||
"curve": default_key.get("curve"),
|
||||
})
|
||||
except ValueError as exc:
|
||||
raise ValidationError(str(exc)) from exc
|
||||
merged["default_key"] = {
|
||||
"type": parsed_default_key["key_type"],
|
||||
"size": parsed_default_key["size"],
|
||||
"curve": parsed_default_key["curve"],
|
||||
}
|
||||
try:
|
||||
bits = int(merged.get("serial_len_bits", 64))
|
||||
except (TypeError, ValueError) as exc:
|
||||
@@ -498,8 +520,13 @@ def parse_key_generation(payload: Mapping[str, Any]) -> Dict[str, Any]:
|
||||
except (TypeError, ValueError) as exc:
|
||||
raise ValidationError("key size must be an integer") from exc
|
||||
curve = payload.get("curve")
|
||||
if key_type == "ec" and curve not in key_core.EC_CURVES:
|
||||
raise ValidationError("unsupported EC curve")
|
||||
if key_type == "ec":
|
||||
try:
|
||||
curve = canonical_curve_name(str(curve or "secp256r1"))
|
||||
except ValueError as exc:
|
||||
raise ValidationError(str(exc)) from exc
|
||||
else:
|
||||
curve = None
|
||||
name = str(payload.get("name") or "").strip()
|
||||
if not name:
|
||||
name = f"{key_type.upper()} {size}" if key_type != "ec" else f"EC {curve}"
|
||||
@@ -567,9 +594,19 @@ async def key_public_object(key: Key) -> Any:
|
||||
|
||||
def _public_part(key_obj: Any) -> Any:
|
||||
"""Return a public key object from either a private or public key."""
|
||||
if hasattr(key_obj, "public_key"):
|
||||
return key_obj.public_key()
|
||||
return key_obj
|
||||
return key_core.public_part(key_obj)
|
||||
|
||||
|
||||
def resolve_signature_hash(
|
||||
key_obj: Any,
|
||||
requested: Any = None,
|
||||
default: Optional[str] = "sha256",
|
||||
) -> Optional[str]:
|
||||
"""Resolve an omitted default or validate an explicit hash for this key."""
|
||||
kind = key_core.key_type_name(key_obj)
|
||||
if requested not in (None, ""):
|
||||
return validate_signature_hash(kind, str(requested))
|
||||
return default_signature_hash(kind, default)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -691,6 +728,7 @@ async def issue_certificate(
|
||||
user: User,
|
||||
) -> Certificate:
|
||||
"""Create a certificate from the wizard payload."""
|
||||
settings_obj = vault_settings(vault)
|
||||
name = str(payload.get("name") or "").strip()
|
||||
if not name:
|
||||
raise ValidationError("name is required")
|
||||
@@ -722,18 +760,33 @@ async def issue_certificate(
|
||||
if key_model.has_private and (signer is None or payload.get("use_key_private", True)):
|
||||
key_obj = await load_private_key(key_model)
|
||||
elif new_key_payload:
|
||||
new_key_params = parse_key_generation({
|
||||
"key_type": new_key_payload.get("key_type") or "rsa",
|
||||
"size": new_key_payload.get("size", key_core.DEFAULT_KEY_SIZE),
|
||||
"curve": new_key_payload.get("curve"),
|
||||
"name": new_key_payload.get("name") or name,
|
||||
})
|
||||
try:
|
||||
generated = await generate_key(
|
||||
str(new_key_payload.get("key_type") or "rsa"),
|
||||
int(new_key_payload.get("size") or key_core.DEFAULT_KEY_SIZE),
|
||||
new_key_payload.get("curve"),
|
||||
new_key_params["key_type"],
|
||||
new_key_params["size"],
|
||||
new_key_params["curve"],
|
||||
)
|
||||
if signer is None:
|
||||
validation_key_type = key_core.key_type_name(generated)
|
||||
else:
|
||||
assert signer_key is not None # validated above for CA-signed issuance
|
||||
validation_key_type = signer_key.key_type
|
||||
if payload.get("signature_hash") not in (None, ""):
|
||||
validate_signature_hash(validation_key_type, str(payload.get("signature_hash")))
|
||||
else:
|
||||
default_signature_hash(validation_key_type, settings_obj.get("default_hash", "sha256"))
|
||||
except ValueError as exc:
|
||||
raise ValidationError(str(exc)) from exc
|
||||
key_model = await save_key(
|
||||
vault,
|
||||
generated,
|
||||
name=str(new_key_payload.get("name") or name),
|
||||
name=new_key_params["name"],
|
||||
comment="Generated while creating a certificate",
|
||||
source="generated",
|
||||
)
|
||||
@@ -760,7 +813,6 @@ async def issue_certificate(
|
||||
extensions = ext_core.extension_specs(req_core.parse_csr(bytes(request_obj.req_der)).extensions)
|
||||
|
||||
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
|
||||
@@ -772,12 +824,13 @@ async def issue_certificate(
|
||||
serial = await unique_serial(vault, signer, bits)
|
||||
elif await _serial_in_use(vault, signer, serial):
|
||||
raise ValidationError("serial number is already in use or revoked by the issuer")
|
||||
signature_hash = payload.get("signature_hash") or settings_obj.get("default_hash", "sha256")
|
||||
|
||||
if signer is None:
|
||||
if key_model is None or not key_model.has_private:
|
||||
raise ValidationError("a self-signed certificate requires the private key")
|
||||
private_key = await load_private_key(key_model)
|
||||
signature_hash = resolve_signature_hash(
|
||||
private_key, payload.get("signature_hash"), settings_obj.get("default_hash", "sha256")
|
||||
)
|
||||
cert = cert_core.build_self_signed(
|
||||
subject=subject,
|
||||
private_key=private_key,
|
||||
@@ -790,6 +843,9 @@ async def issue_certificate(
|
||||
else:
|
||||
signer_private = await load_private_key(signer_key)
|
||||
signer_cert = cert_core.parse_certificate(bytes(signer.cert_der))
|
||||
signature_hash = resolve_signature_hash(
|
||||
signer_private, payload.get("signature_hash"), settings_obj.get("default_hash", "sha256")
|
||||
)
|
||||
cert = cert_core.build_signed(
|
||||
subject=subject,
|
||||
public_key=_public_part(key_obj),
|
||||
@@ -895,7 +951,7 @@ async def preview_certificate(vault: Vault, payload: Mapping[str, Any]) -> Dict[
|
||||
)
|
||||
else:
|
||||
generated = await generate_key("ec", curve="secp256r1")
|
||||
public_key = generated.public_key()
|
||||
public_key = key_core.public_part(generated)
|
||||
|
||||
extensions_spec = parse_extensions(payload.get("extensions"))
|
||||
if request_obj is not None and payload.get("copy_request_extensions"):
|
||||
@@ -951,16 +1007,21 @@ async def renew_certificate(
|
||||
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))
|
||||
signature_hash = resolve_signature_hash(
|
||||
signer_private,
|
||||
payload.get("signature_hash"),
|
||||
cert_core.signature_hash_name(parsed) or "sha256",
|
||||
)
|
||||
new_cert = cert_core.build_certificate(
|
||||
subject=parsed.subject,
|
||||
public_key=parsed.public_key(),
|
||||
public_key=cert_core.certificate_public_key(parsed),
|
||||
issuer_cert=None if signer.id == cert.id else signer_cert,
|
||||
issuer_private_key=signer_private,
|
||||
serial=serial,
|
||||
not_before=not_before,
|
||||
not_after=not_after,
|
||||
extensions=list(parsed.extensions),
|
||||
signature_hash=payload.get("signature_hash") or cert_core._signature_hash_name(parsed),
|
||||
signature_hash=signature_hash,
|
||||
)
|
||||
info = cert_core.certificate_info(new_cert)
|
||||
row = await Certificate.create(
|
||||
@@ -1111,9 +1172,15 @@ async def generate_crl(vault: Vault, ca: Certificate, payload: Mapping[str, Any]
|
||||
next_update = last_update + timedelta(days=days)
|
||||
crl_number = int(payload.get("crl_number") or (authority.crl_number + 1))
|
||||
revocations = await CertificateRevocation.filter(ca_id=ca.id)
|
||||
issuer_private = await load_private_key(key)
|
||||
signature_hash = resolve_signature_hash(
|
||||
issuer_private,
|
||||
payload.get("signature_hash"),
|
||||
vault_settings(vault).get("default_hash", "sha256"),
|
||||
)
|
||||
crl = crl_core.build_crl(
|
||||
issuer_cert=cert_core.parse_certificate(bytes(ca.cert_der)),
|
||||
issuer_private_key=await load_private_key(key),
|
||||
issuer_private_key=issuer_private,
|
||||
revocations=[
|
||||
{
|
||||
"serial_hex": row.serial_hex,
|
||||
@@ -1126,7 +1193,7 @@ async def generate_crl(vault: Vault, ca: Certificate, payload: Mapping[str, Any]
|
||||
last_update=last_update,
|
||||
next_update=next_update,
|
||||
crl_number=crl_number,
|
||||
signature_hash=payload.get("signature_hash") or vault_settings(vault).get("default_hash", "sha256"),
|
||||
signature_hash=signature_hash,
|
||||
with_reason=bool(payload.get("with_reason", True)),
|
||||
include_aki=bool(payload.get("include_aki", True)),
|
||||
include_issuer_alt_name=bool(payload.get("include_issuer_alt_name", True)),
|
||||
|
||||
Reference in New Issue
Block a user