Skip to content

Add dedicated writer for certificate validation - #1517

Merged
ivosh merged 9 commits into
mainfrom
feature/cert-validation-writer
May 28, 2026
Merged

Add dedicated writer for certificate validation#1517
ivosh merged 9 commits into
mainfrom
feature/cert-validation-writer

Conversation

@ivosh

@ivosh ivosh commented May 26, 2026

Copy link
Copy Markdown
Collaborator

What the old code got wrong

Before this PR, both X509CertificateValidator.finalizeValidation and the catch-path of CertificateService.validate wrote validation results by calling repository.save(certificate) on an in-memory entity snapshot. save delegates to em.merge, which writes every column back to the database — including state. If a concurrent operator-driven revoke had already committed state = PENDING_REVOKE between the time the validate path loaded the entity and the time it called save, the merge would silently overwrite it with the stale ISSUED snapshot the validator was holding.

Thread A: validate(cert)                  Thread B: revoke(cert)
──────────────────────────────────        ──────────────────────
read cert entity (state = ISSUED)
  ... validate, lots of I/O ...
                                          state = PENDING_REVOKE  ← committed
em.merge(stale entity)  ─────────────────────────► state = ISSUED  ← lost update
COMMIT

The same structural problem appeared in the OCSP/CRL-driven revocation path inside finalizeValidation: after detecting a REVOKED status from OCSP or CRL, the validator would directly write state = REVOKED via save, again overwriting the row wholesale instead of doing a conditional update.

What this PR does, and where it sits in the path to the fix

This PR is PR 2 of a 5-PR sequence that converges on a clean, race-free revoke fix. It introduces CertificateValidationWriter — the writer bean for the validation path — and the three targeted @Modifying @Query methods it needs in CertificateRepository.

┌────────────────────────────────────────┐    ┌──────────────────────────────────────────┐
│ X509CertificateValidator               │    │ CertificateValidationWriter               │
│ (no @Transactional — runs in validate's│───▶│ (no class annotation)                     │
│  NOT_SUPPORTED ambient context)        │    │ • @Transactional                           │
│ • finalizeValidation                   │    │   applyValidationResult(...)              │
│   → writer.applyValidationResult       │    │     — updates only: validationStatus,     │
│   → writer.markRevokedIfStillIssued    │    │       statusValidationTimestamp,           │
│     + classifyZeroRowOutcome           │    │       certificateValidationResult, updated │
└────────────────────────────────────────┘    │ • @Transactional                           │
                                              │   markRevokedIfStillIssued(uuid)           │
                                              │     — conditional UPDATE:                  │
                                              │       WHERE state = ISSUED                 │
                                              │       SET   state = REVOKED                │
                                              │     — returns rows affected (1 or 0)       │
                                              └──────────────────────────────────────────┘

applyValidationResult is a targeted UPDATE that touches only the three validation columns plus the audit timestamp — it never reads or writes state. A concurrent revoke's state = PENDING_REVOKE survives intact regardless of when the validate path commits.

markRevokedIfStillIssued is a compare-and-swap: it only transitions the row from ISSUED → REVOKED if the row is still in that exact state. A 0-row return means something else got there first; classifyZeroRowOutcome reads back the current state and decides whether the intent was already fulfilled by a concurrent path (REVOKED / PENDING_REVOKE) or whether the observed state is genuinely diverged and needs a reconciliation warning.

OCSP/CRL says REVOKED:

  markRevokedIfStillIssued(uuid)
    ├─ 1 row → committed ISSUED → REVOKED
    └─ 0 rows → read state
                ├─ REVOKED / PENDING_REVOKE → INTENT_ALREADY_SATISFIED (log info)
                └─ anything else           → STATE_DIVERGENCE (log warn)

PR sequence to the revoke fix:

PR #1512        ──►  PR #1517 (this)   ──►  PR 3          ──►  PR 4         ──►  PR 5
Chain service        Validation             CrlService          Drop redundant     Revoke fix
+ writer             writer                 refactor + writer   repo-level         lands cleanly
                                                                @Transactional     (0 repo-level
                                                                on 5 methods       @Transactional
                                                                                   added)

Tests

  • CertificateValidationWriterTxTest — AOP proxy guard (validates the writer is Spring-proxied so @Transactional advice fires), applyValidationResult persists all three columns and refreshes updated, markRevokedIfStillIssued transitions ISSUED → REVOKED (1 row) and is a no-op for PENDING_REVOKE (0 rows).
  • ValidationResultVsRevokeTest — deterministic simulation of the race: insert ISSUED, JDBC-UPDATE state to PENDING_REVOKE (models concurrent revoke commit), call applyValidationResult with a stale view, assert state remains PENDING_REVOKE and validationStatus was written correctly.
  • X509CertificateValidatorZeroRowOutcomeTest — parametrized unit tests for classifyZeroRowOutcome: REVOKED/PENDING_REVOKEINTENT_ALREADY_SATISFIED; all other states (including null for a deleted row) → STATE_DIVERGENCE.

@ivosh ivosh self-assigned this May 26, 2026
Copilot AI review requested due to automatic review settings May 26, 2026 20:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a dedicated CertificateValidationWriter to persist certificate validation results (and an OCSP/CRL-driven ISSUED→REVOKED transition) via targeted database updates, aiming to avoid clobbering concurrently updated certificate columns (notably state).

Changes:

  • Added CertificateValidationWriter service with transactional writer methods for validation results and conditional revocation transition.
  • Refactored X509CertificateValidator (and an exception path in CertificateServiceImpl) to use targeted UPDATEs instead of entity saves for validation persistence and revocation transitions.
  • Added/updated tests covering concurrency-oriented behavior and transactional proxying.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/main/java/com/czertainly/core/service/writer/CertificateValidationWriter.java New transactional writer service for targeted certificate validation/state updates.
src/main/java/com/czertainly/core/validation/certificate/X509CertificateValidator.java Uses writer for validation persistence and conditional revocation; adds zero-row outcome classification/logging.
src/main/java/com/czertainly/core/service/impl/CertificateServiceImpl.java Uses writer on validation failure path instead of saving the entity.
src/main/java/com/czertainly/core/dao/repository/CertificateRepository.java Adds JPQL bulk update methods for validation result writes, conditional state transition, and state read-back.
src/test/java/com/czertainly/core/validation/certificate/X509CertificateValidatorZeroRowOutcomeTest.java New unit test for zero-row outcome classification.
src/test/java/com/czertainly/core/service/writer/ValidationResultVsRevokeTest.java New integration test simulating validate-vs-revoke race and ensuring state isn’t clobbered.
src/test/java/com/czertainly/core/service/writer/CertificateValidationWriterTxTest.java New integration test verifying transactional proxying and writer behavior.
src/test/java/com/czertainly/core/service/writer/CertificateChainWriterTxTest.java Test method renames and minor assertion message change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/main/java/com/czertainly/core/dao/repository/CertificateRepository.java Outdated
Comment thread src/main/java/com/czertainly/core/dao/repository/CertificateRepository.java Outdated
ivosh and others added 2 commits May 26, 2026 23:04
- Add clearAutomatically/flushAutomatically to @Modifying on updateValidationResult
  and transitionIssuedToRevoked -- prevents stale managed entity from re-flushing
  a full UPDATE after a JPQL bulk UPDATE, which would overwrite concurrently changed columns
- Fix misleading comment in X509CertificateValidator: state read-back runs within the
  same transaction (REQUIRED propagation), not outside the writer's tx
- Add ValidationResultVsRevokeTest case covering the ambient-tx entity-dirtying scenario

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

ivosh and others added 2 commits May 27, 2026 09:05
- Sync in-memory certificate.certificateValidationResult to null on the
  exception path to match the DB write from applyValidationResult
- Fix inaccurate "within the same transaction" comment in X509CertificateValidator:
  the read-back after markRevokedIfStillIssued is a separate read, not part of
  a shared transaction

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.

@lubomirw lubomirw left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review: 3 advisory finding(s). All non-blocking — well-structured PR with thorough tests.

@sonarqubecloud

Copy link
Copy Markdown

@ivosh
ivosh merged commit 0f40001 into main May 28, 2026
14 checks passed
@ivosh
ivosh deleted the feature/cert-validation-writer branch May 28, 2026 02:05
@lubomirw lubomirw changed the title Introduce CertificateValidationWriter Add dedicated writer for certificate validation Jun 8, 2026
@lubomirw lubomirw added the ignore-for-release Exclude from auto-generated release notes label Jun 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ignore-for-release Exclude from auto-generated release notes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Certificates - Revoke: first Revoke attempt is silently dropped when the RA Profile has a Compliance Profile attached

3 participants