Skip to content

Sync guard GatewayCanParseSubject fails to block CN ending in odd-count trailing backslash (1.3.3) #30

Description

@spbsoluble

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:

  1. Deploy AnyCA Gateway REST on a fresh host + empty gateway DB; drop GCPCASCAPlugin.dll FileVersion 1.3.3.0 into Extensions\GCPCAS\.
  2. Configure a CA against a GCP CAS Enterprise pool.
  3. 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).
  4. Run the gateway CASyncJob. Observe Synced N certificates and skipped 0 and no [SYNC-SKIP] for the new cert.
  5. Confirm [Certificates].[IssuedSubject] for the new row ends in bytes 5C 00 5C 00.
  6. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions