Summary
The 1.3.3 sync guard GatewayCanParseSubject (GCPCAS/Client/GCPCASClient.cs:446) does not block the subject shape it was added to block: a Common Name ending in an odd number of literal backslash bytes (e.g. one trailing \). Such certificates are still admitted into the plugin's sync buffer, persisted by the gateway to [dbo].[Certificates].[IssuedSubject], and then abort Command's Full Scan with badly formatted directory string (HTTP 500) — exactly the failure mode the guard was meant to close.
Confirmed against tag 1.3.3. The guard code is byte-identical at 1.3.3 and at the PR #27 branch tip (git diff 1.3.3 <pr27-tip> -- GCPCAS/Client/GCPCASClient.cs is empty), so PR #27 (merge-marker cleanup) does not touch this path — the defect survives it.
Root cause
The guard validates the pre-storage subject string form, but the sync abort surfaces on the post-storage form. The two differ by one escape/un-escape round-trip, and the guard never exercises the form that actually throws.
// GCPCAS/Client/GCPCASClient.cs:446
private bool GatewayCanParseSubject(string pem, out string subject, out string failureReason)
{
...
using X509Certificate2 netCert = X509Certificate2.CreateFromPem(pem);
subject = netCert.Subject; // (1)
_ = new Org.BouncyCastle.Asn1.X509.X509Name(true, subject); // (2)
return true;
...
}
For a CN ending in one literal backslash byte, the escaped form the gateway stores parses fine, but the gateway's search-response formatter (ConvertSubjectFromCommandToEJBCAFormat) later un-escapes one level and re-parses CN=…\ with a second X509Name construction. The lone trailing \ is an unmatched escape at end-of-input → X509NameTokenizer.NextToken() throws ArgumentException("badly formatted directory string") → HTTP 500 → Command's LocalCASyncJob aborts.
The guard only exercised the pre-storage form (which parses) and never the un-escaped form (which is what reaches BouncyCastle on the failing response path). Result: Synced N certificates and skipped 0, no [SYNC-SKIP] line, guard never fires.
Note: the exact escaping attribution above was refined during implementation — see the follow-up comment. .NET renders literal backslashes verbatim; the escape-doubling is the gateway's store step. The observable trigger (odd-length backslash run) is unchanged.
Reproduction (lab)
Fresh 1.3.3 install, fresh empty gateway DB, certificates issued directly on GCP CAS (bypassing the plugin enrollment path so upstream sync is the only code path exercised).
Test subject shapes were issued; all were admitted, only the odd-trailing-backslash shape aborted the scan:
shape1.lab.test\ — CN ending in one literal backslash ← trips the bug
shape2.lab.test\\ — two trailing backslashes
shape3.lab.test\\\\,OU=PKI,O=Keyfactor Labs — four backslashes mid-value
CN=CN=shape4.lab.test:oracle_wallet,O=Keyfactor Labs,C=US,C=US — nested DN in outer CN
Steps:
- Deploy AnyCA Gateway REST on a fresh host + empty gateway DB; drop
GCPCASCAPlugin.dll FileVersion 1.3.3.0 into Extensions\GCPCAS\.
- Configure a CA against a GCP CAS Enterprise pool.
- Out of band, issue a cert on the pool whose CN ends in one literal backslash byte (
gcloud privateca certificates create …; GCP CAS accepts it without RFC 4514 validation).
- Run the gateway
CASyncJob. Observe Synced N certificates and skipped 0 and no [SYNC-SKIP] for the new cert.
- Confirm
[Certificates].[IssuedSubject] for the new row ends in bytes 5C 00 5C 00.
- Trigger a CA Sync from Command →
LocalCASyncJob aborts with There was an error retrieving the certificates …; gateway log shows Converting the subject DN to EJBCA format: CN=…\ followed by the badly formatted directory string stack trace ending at CertificateV2Controller.CertificateSearch. Deterministic on every subsequent scan.
Observed stack trace (gateway side):
badly formatted directory string
at Org.BouncyCastle.Asn1.X509.X509NameTokenizer.NextToken()
at Org.BouncyCastle.Asn1.X509.X509Name..ctor(Boolean reverse, IDictionary`2 lookup, String dirName, X509NameEntryConverter converter)
at Keyfactor.EJBCA.Core.EJBCAUtilities.ConvertSubjectFromCommandToEJBCAFormat(String subject)
at Keyfactor.EJBCA.Core.EJBCAUtilities.ConvertSubjectFromCommandToEJBCAAPIResponseFormat(String subject)
at AnyGatewayREST.Controllers.CertificateV2Controller.CertificateSearch(CertificateSearchV2Request certificateSearchRequest)
Impact
CA Sync for an affected CA cannot complete: a single certificate with this subject shape 500s the gateway's certificate-search page, which aborts the entire Full Scan without advancing the last-scan watermark. The failure is deterministic and recurs on every subsequent scan, and re-materialises after DB-side removal because the plugin re-admits the row on the next upstream sync.
Proposed fix (plugin-side)
The guard must reject the shape the gateway cannot round-trip on its response path:
- Reject CN/attribute values that contain an odd-length run of literal backslash bytes, since those are exactly the shapes whose escape/un-escape round-trip is not idempotent; and/or
- probe the un-escaped form (not just the pre-storage form) with
X509Name.
When a cert is rejected, keep emitting the existing [SYNC-SKIP] warning so operators can see which upstream certs are being held back.
Underlying defect (context, not the fix here)
The real asymmetry is in the AnyCA Gateway REST itself: it stores the escaped subject but its response formatter un-escapes once and re-parses the result with X509Name, so any subject whose escaping is not round-trip-idempotent 500s the search endpoint. The plugin guard is a defensive filter in front of that gateway behaviour; a durable fix ideally also addresses the gateway parse path, which should be raised with that team.
Basis: tag 1.3.3 (not main, whose merge-marker poisoning is addressed by PR #27).
Summary
The 1.3.3 sync guard
GatewayCanParseSubject(GCPCAS/Client/GCPCASClient.cs:446) does not block the subject shape it was added to block: a Common Name ending in an odd number of literal backslash bytes (e.g. one trailing\). Such certificates are still admitted into the plugin's sync buffer, persisted by the gateway to[dbo].[Certificates].[IssuedSubject], and then abort Command's Full Scan withbadly formatted directory string(HTTP 500) — exactly the failure mode the guard was meant to close.Confirmed against tag
1.3.3. The guard code is byte-identical at1.3.3and at the PR #27 branch tip (git diff 1.3.3 <pr27-tip> -- GCPCAS/Client/GCPCASClient.csis empty), so PR #27 (merge-marker cleanup) does not touch this path — the defect survives it.Root cause
The guard validates the pre-storage subject string form, but the sync abort surfaces on the post-storage form. The two differ by one escape/un-escape round-trip, and the guard never exercises the form that actually throws.
For a CN ending in one literal backslash byte, the escaped form the gateway stores parses fine, but the gateway's search-response formatter (
ConvertSubjectFromCommandToEJBCAFormat) later un-escapes one level and re-parsesCN=…\with a secondX509Nameconstruction. The lone trailing\is an unmatched escape at end-of-input →X509NameTokenizer.NextToken()throwsArgumentException("badly formatted directory string")→ HTTP 500 → Command'sLocalCASyncJobaborts.The guard only exercised the pre-storage form (which parses) and never the un-escaped form (which is what reaches BouncyCastle on the failing response path). Result:
Synced N certificates and skipped 0, no[SYNC-SKIP]line, guard never fires.Reproduction (lab)
Fresh 1.3.3 install, fresh empty gateway DB, certificates issued directly on GCP CAS (bypassing the plugin enrollment path so upstream sync is the only code path exercised).
Test subject shapes were issued; all were admitted, only the odd-trailing-backslash shape aborted the scan:
shape1.lab.test\— CN ending in one literal backslash ← trips the bugshape2.lab.test\\— two trailing backslashesshape3.lab.test\\\\,OU=PKI,O=Keyfactor Labs— four backslashes mid-valueCN=CN=shape4.lab.test:oracle_wallet,O=Keyfactor Labs,C=US,C=US— nested DN in outer CNSteps:
GCPCASCAPlugin.dllFileVersion1.3.3.0intoExtensions\GCPCAS\.gcloud privateca certificates create …; GCP CAS accepts it without RFC 4514 validation).CASyncJob. ObserveSynced N certificates and skipped 0and no[SYNC-SKIP]for the new cert.[Certificates].[IssuedSubject]for the new row ends in bytes5C 00 5C 00.LocalCASyncJobaborts withThere was an error retrieving the certificates …; gateway log showsConverting the subject DN to EJBCA format: CN=…\followed by thebadly formatted directory stringstack trace ending atCertificateV2Controller.CertificateSearch. Deterministic on every subsequent scan.Observed stack trace (gateway side):
Impact
CA Sync for an affected CA cannot complete: a single certificate with this subject shape 500s the gateway's certificate-search page, which aborts the entire Full Scan without advancing the last-scan watermark. The failure is deterministic and recurs on every subsequent scan, and re-materialises after DB-side removal because the plugin re-admits the row on the next upstream sync.
Proposed fix (plugin-side)
The guard must reject the shape the gateway cannot round-trip on its response path:
X509Name.When a cert is rejected, keep emitting the existing
[SYNC-SKIP]warning so operators can see which upstream certs are being held back.Underlying defect (context, not the fix here)
The real asymmetry is in the AnyCA Gateway REST itself: it stores the escaped subject but its response formatter un-escapes once and re-parses the result with
X509Name, so any subject whose escaping is not round-trip-idempotent 500s the search endpoint. The plugin guard is a defensive filter in front of that gateway behaviour; a durable fix ideally also addresses the gateway parse path, which should be raised with that team.Basis: tag
1.3.3(notmain, whose merge-marker poisoning is addressed by PR #27).