Reject duplicate serials and make keep_serial renewal safe
Add shared serial collision checking against existing certificates and issuer revocations, reuse it for automatic serial allocation, and enforce it for user-supplied serials so duplicate serials no longer break the OCSP snapshot. make keep_serial renewal require replace_old, refuse keep_serial + revoke_old, and reject serials already revoked by the issuer so revoked serials cannot be inherited by renewed certificates.
This commit is contained in:
@@ -608,17 +608,29 @@ def validate_validity(payload: Mapping[str, Any]) -> Tuple[datetime, datetime, b
|
||||
return not_before, not_after, no_expiration
|
||||
|
||||
|
||||
async def _serial_in_use(
|
||||
vault: Vault,
|
||||
issuer: Optional[Certificate],
|
||||
serial: int,
|
||||
*,
|
||||
exclude_certificate_id: Optional[UUID] = None,
|
||||
) -> bool:
|
||||
text = format(serial, "x")
|
||||
certificates = Certificate.filter(vault_id=vault.id, serial_hex=text)
|
||||
if exclude_certificate_id is not None:
|
||||
certificates = certificates.exclude(id=exclude_certificate_id)
|
||||
if await certificates.exists():
|
||||
return True
|
||||
return issuer is not None and await CertificateRevocation.filter(ca_id=issuer.id, serial_hex=text).exists()
|
||||
|
||||
|
||||
async def unique_serial(vault: Vault, issuer: Optional[Certificate], bits: int) -> int:
|
||||
for _ in range(100):
|
||||
serial = secrets.randbits(bits)
|
||||
if serial <= 0:
|
||||
continue
|
||||
text = format(serial, "x")
|
||||
if await Certificate.filter(vault_id=vault.id, serial_hex=text).exists():
|
||||
continue
|
||||
if issuer is not None and await CertificateRevocation.filter(ca_id=issuer.id, serial_hex=text).exists():
|
||||
continue
|
||||
return serial
|
||||
if not await _serial_in_use(vault, issuer, serial):
|
||||
return serial
|
||||
raise ValidationError("failed to allocate a unique serial number")
|
||||
|
||||
|
||||
@@ -743,6 +755,8 @@ async def issue_certificate(
|
||||
raise ValidationError("serial must be an integer or a hexadecimal string") from exc
|
||||
if serial <= 0:
|
||||
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:
|
||||
@@ -903,7 +917,21 @@ async def renew_certificate(
|
||||
raise ValidationError("the certificate's issuer has no private key")
|
||||
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)))
|
||||
revoke_old = bool(payload.get("revoke_old"))
|
||||
replace_old = bool(payload.get("replace_old"))
|
||||
if keep_serial and revoke_old:
|
||||
raise ValidationError("keep_serial cannot be combined with revoke_old")
|
||||
if keep_serial:
|
||||
serial = parsed.serial_number
|
||||
if await _serial_in_use(
|
||||
vault,
|
||||
signer,
|
||||
serial,
|
||||
exclude_certificate_id=cert.id if replace_old else None,
|
||||
):
|
||||
raise ValidationError("serial number is already in use or revoked by the issuer")
|
||||
else:
|
||||
serial = await unique_serial(vault, signer, int(vault_settings(vault).get("serial_len_bits", 64)))
|
||||
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)
|
||||
@@ -955,9 +983,9 @@ async def renew_certificate(
|
||||
)
|
||||
if info.is_ca:
|
||||
await Authority.create(certificate_id=row.id, crl_days=30)
|
||||
if payload.get("revoke_old") and signer.id != cert.id:
|
||||
if revoke_old and signer.id != cert.id:
|
||||
await revoke_certificate(vault, cert, {"reason": payload.get("revocation_reason", "superseded")})
|
||||
if payload.get("replace_old"):
|
||||
if replace_old:
|
||||
await delete_certificate_cascade(vault, cert)
|
||||
await relink_certificates(vault)
|
||||
if signer.id != cert.id:
|
||||
|
||||
Reference in New Issue
Block a user