Skip to content

fix(sync): V1 body-less REVOKED guard and incremental skip of unchanged orders (1.0.2) - #38

Open
spbsoluble wants to merge 5 commits into
release-1.0from
fix/v1.0.2-v1-bodyless-revoked
Open

spbsoluble wants to merge 5 commits into
release-1.0from
fix/v1.0.2-v1-bodyless-revoked

Conversation

@spbsoluble

@spbsoluble spbsoluble commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The V1 client never downloads a revoked certificate's body: CERTInext refuses it ("Invalid request status."), and IsCertificateDownloadable excludes status 22. So Synchronize and GetSingleRecord emit every revoked order with no certificate.

That's safe when the gateway already stores the certificate. When it doesn't (the order was revoked at CERTInext before the gateway recorded it as issued), the gateway saves a row with RevocationDate set and no certificate, and never clears that date. Its revoked-certificate search (POST .../v2/certificate/search) then fails with m_safeCertContext is an invalid handle at CertificateV2Controller.CertificateSearch. Every Command scan of the CA fails from then on, not just the affected record.

This ports the V2 fix for issue 0049 to the V1 code path.

Fix

Before emitting a revoked record with no certificate, the plugin asks the gateway whether it already stores the certificate (ICertificateDataReader.GetExpirationDateByRequestId):

Gateway state Sync GetSingleRecord
Holds the certificate (returns a date) emit REVOKED unchanged (out-of-band revokes still propagate) REVOKED
Row exists, no certificate (returns null) emit FAILED, no RevocationDate/Reason FAILED
No row (ArgumentException), other error, or no reader skip FAILED

The sync summary log now includes FailedFromBodylessRevoked and BodylessRevokedSkipped.

Not covered

  • Rows already broken. The gateway never clears RevocationDate, so the plugin can't repair rows that are already bad. They need a one-time DB repair: set Status = 11 and RevocationDate = NULL on rows with a RevocationDate and no certificate. The repair was tested end to end on a 25.5.0 lab gateway.
  • Upgrading the gateway doesn't help. AnyGateway 26.2.1 has the same revoked-search query and controller loop (decompiled), so this guard is still needed after a gateway upgrade.
  • Enroll. The pickup poll can still return a REVOKED result with no certificate if an order is revoked during pickup. That's left for a follow-up.

Testing

  • 11 new unit tests (BodylessRevokedGuardTests) cover each row of the table above, plus a failed refetch and a revoked record that has a body. 8 of them fail against the 1.0.1 code. Full suite: 524/524 pass.
  • Release build: no new code warnings.
  • Opt-in, read-only live probe (RevokedDownloadProbeTests, CERTINEXT_PROBE_REVOKED_DOWNLOAD=1): sandbox V1 download of a revoked order was refused, confirming there's no certificate to backfill.

Incremental sync refetches every order placed that day (0093)

Problem

The V1 order report filters only by calendar day (orderDateFrom is sent as yyyy-MM-dd), so every incremental pass re-lists every order placed that day. The listing has no certificate, so each issued or revoked order then costs a TrackOrder plus a DownloadCertificate, one after another, even when the gateway already holds it unchanged.

Seen in a customer log on 2026-11-10: about 380 orders were placed in CERTInext outside the gateway, and from then on every 10-minute pass processed about 225 orders and made 451 calls (about 8.5 minutes per pass). When CERTInext latency rose that afternoon (1.2 s to 47.6 s average), passes overran the interval and ran back to back. This is not a 1.0.1 regression: the date filter and per-order refetch date from the initial implementation.

Fix

Same rule as the Entrust ECS, HashiCorp Vault and Nexus plugins. On incremental passes only, an order listed as GENERATED or REVOKED is skipped when ICertificateDataReader.GetStatusByRequestID returns the same status and GetExpirationDateByRequestId shows a stored certificate.

Processed as before: no gateway row (new order), a status change (pending to issued, issued to revoked), a row with no certificate, no reader, or a reader error. Pending orders and full sync are unchanged. The sync summary log now includes UnchangedInGateway.

Not changed: a pass still lists the whole day's orders, because the order report has no time or modified-since filter. The cost per issued order drops from two CERTInext calls to two gateway DB lookups.

Testing

  • 10 new unit tests (IncrementalSyncUnchangedTests): unchanged issued and revoked orders skipped, new order, both status changes, row without a certificate, reader error, no reader, pending order, and full sync.
  • Full suite: 534/534 pass. A first run had 2 failures in unrelated WireMock-based tests (TracePayloadGuardTests, ValidityResolutionTests); both pass alone and on a second full run.
  • Release build: no code warnings (only NU1801, the GitHub package feed was unreachable from the build machine).
  • Not yet run against live CERTInext or a gateway.

Docs: change of issuing CA after eMudhra's exit from public TLS

eMudhra is withdrawing from public SSL/TLS, and the emSign G1 root is being phased out: Chrome and Firefox won't trust G1 certificates logged after 2026-11-16, and Apple removes the root on 2026-11-30. CERTInext public TLS certificates move to another CA (SSL.com for InCommon members; eMudhra offers Sectigo).

docsource/configuration.md and docsource/overview.md now say the plugin needs no change, because it stores only the end-entity certificate and never reads the issuer. They list the operator steps: import the new chain on Command, the gateway and orchestrators; update the Gateway Registration CA certificate; replace G1 certificates before Apple's removal date; and confirm product codes with CERTInext. The README is regenerated from docsource.

…hold (0049, V1)

The V1 client never downloads a revoked certificate's body (CERTInext refuses it,
and IsCertificateDownloadable excludes status 22), so Synchronize and
GetSingleRecord emitted every revoked order with Certificate = null. For an order
the gateway never stored with a body (revoked at CERTInext before the gateway saw
it issued), the gateway persists a row with RevocationDate set and no certificate,
never clears that date, and its revoked-certificate search then fails with
"m_safeCertContext is an invalid handle" on every Command scan of the CA.

Port the V2 0049 guard to V1: before emitting a body-less REVOKED record, ask
ICertificateDataReader.GetExpirationDateByRequestId whether the gateway holds a body.
  - date returned (gateway holds a body) -> emit REVOKED unchanged, so out-of-band
    CA revokes still propagate.
  - null (row exists, no body) -> emit FAILED with no RevocationDate/Reason.
  - ArgumentException (no row), any other error, or no reader -> Sync skips the
    record; GetSingleRecord returns FAILED (it can't skip).
The sync summary now reports FailedFromBodylessRevoked and BodylessRevokedSkipped.

Rows already written this way are not repaired by the plugin (the gateway never
nulls RevocationDate); they need the documented DB repair. Gateway 26.2.1 has the
same revoked-search query, so the guard is still needed after a gateway upgrade.
…body (0049)

Read-only. CERTINEXT_PROBE_REVOKED_DOWNLOAD=1 calls DownloadCertificate directly on
a revoked order (CERTINEXT_REVOKED_ORDER_NUMBER, or the first revoked order in the
report). Sandbox 2026-11-05: refused with "Invalid request status.", confirming
there is no body to backfill.
@spbsoluble
spbsoluble force-pushed the fix/v1.0.2-v1-bodyless-revoked branch from 5e816f2 to 2e7f69a Compare November 5, 2026 21:18
spbsoluble and others added 3 commits November 10, 2026 12:12
The V1 order report only filters by calendar day (orderDateFrom = yyyy-MM-dd), so
every incremental pass re-listed every order placed that day and refetched each
issued or revoked one (TrackOrder + DownloadCertificate), even when the gateway
already held it. A customer gateway with ~225 orders in the day made 451 serial
calls per 10-minute pass; when CERTInext latency rose, passes overran the interval
and ran back to back.

Follow the Entrust ECS / HashiCorp Vault / Nexus pattern: on incremental passes,
skip an order listed as GENERATED or REVOKED when
ICertificateDataReader.GetStatusByRequestID returns the same status and
GetExpirationDateByRequestId shows a stored body. New orders, status changes
(pending -> issued, issued -> revoked), rows without a body, no reader, and reader
errors are processed as before. Pending orders and full sync are unchanged. The
sync summary now reports UnchangedInGateway.

Not a regression: the date-only filter and per-order refetch date from the initial
implementation.
eMudhra is withdrawing from public SSL/TLS and the emSign roots are being phased
out (Chrome/Firefox: G1 certificates logged after 2026-11-16 not trusted; Apple:
G1 root removed 2026-11-30). CERTInext public TLS certificates move to another CA
(SSL.com for InCommon members; eMudhra offers Sectigo).

Document that the plugin needs no change (it stores only the end-entity
certificate and never reads the issuer), and the operator steps: import the new
chain on Command, the gateway and orchestrators, update the Gateway Registration CA
certificate, replace G1 certificates before Apple's removal, and confirm product
codes with CERTInext. Links the InCommon and eMudhra notices.
@spbsoluble spbsoluble changed the title fix(sync): never emit a body-less REVOKED record the gateway doesn't hold (1.0.2) fix(sync): V1 body-less REVOKED guard and incremental skip of unchanged orders (1.0.2) Nov 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant