Skip to content

Plugin fails to load on gateway 25.4.0: "Could not load type IDomainValidatorFactory" from Keyfactor.AnyGateway.IAnyCAPlugin v3.2.0.0 #7

Closed

Description

@spbsoluble

Summary

The CERTInext caplugin DLL fails to load inside the AnyCA REST Gateway image 25.4.0 (chart anygateway-rest 1.0.1) with a missing-type error against Keyfactor.AnyGateway.IAnyCAPlugin.

Steps to reproduce

  1. Deploy AnyCA REST Gateway 25.4.0 (chart anygateway-rest 1.0.1).
  2. Stage and load `CERTInext/bin/Release/net10.0/CERTInextCAPlugin.dll` (built locally from the current `main` of this repo) into the gateway pod's `/app/Extensions//` directory.
  3. POST a CA registration to `/AnyGatewayREST/config/configuration` referencing the plugin.

Actual behavior

Gateway returns HTTP 500 with body `{"ErrorCode":"0x80131509"}`. Gateway pod logs show:

```
Could not load type 'Keyfactor.AnyGateway.Extensions.IDomainValidatorFactory'
from assembly 'Keyfactor.AnyGateway.IAnyCAPlugin, Version=3.2.0.0, Culture=neutral, PublicKeyToken=0ed89d330114ab09'.
at System.Signature..ctor(...)
at System.Reflection.RuntimeConstructorInfo.<get_Signature>g__LazyCreateSignature|21_0()
at System.Reflection.RuntimeConstructorInfo.GetParameters()
at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteFactory.<>c.b__27_0(...)
```

The plugin's manifest references the type `Keyfactor.Extensions.CAPlugin.CERTInext.CERTInextCAPlugin` whose constructor (directly or transitively) depends on `IDomainValidatorFactory`. That interface does not exist in the version of `Keyfactor.AnyGateway.IAnyCAPlugin` shipped in gateway image 25.4.0.

Expected behavior

The DLL loads cleanly on a supported gateway version.

Plugin metadata

From `integration-manifest.json` on `main`:

```json
{
"gateway_framework": "24.2.0",
"release_dir": "CERTInext/bin/Release"
}
```

The declared framework target is `24.2.0`, but the plugin code currently consumes a type (`IDomainValidatorFactory`) that exists in a NEWER framework. Either the `gateway_framework` field is out of date OR the plugin needs to either back-port off `IDomainValidatorFactory` or pin its `Keyfactor.AnyGateway.IAnyCAPlugin` NuGet to a version that contains the type.

Environment

  • AnyCA REST Gateway image: `keyfactor.jfrog.io/kyfuseng-release/command/anygateway-rest:25.4.0`
  • Helm chart: `anygateway-rest-1.0.1`
  • Plugin build target framework: `net10.0`
  • Plugin DLL size: 171520 bytes (built locally from `main`)
  • Discovered via the kfclab lab orchestrator's generic plugin pipeline; orchestrator itself is plugin-agnostic, so the failure surfaces in the gateway pod cleanly.

Suggested fix

Pin or bump the `Keyfactor.AnyGateway.IAnyCAPlugin` NuGet reference in `CERTInext/CERTInext.csproj` to a version that includes `IDomainValidatorFactory`, and update `integration-manifest.json#gateway_framework` to match the matching gateway release. Alternately, refactor the plugin to make `IDomainValidatorFactory` an optional dependency.

Activity

  1. github-actions commented on May 21, 2026

    @github-actions
    Contributor

    Thanks for opening this issue! Our team has been notified and will follow up here.

  2. spbsoluble commented on May 21, 2026

    @spbsoluble
    CollaboratorAuthor

    Fix landed on PR #2 in commit ce8e02d.

    What changed

    • The only public constructor on CERTInextCAPlugin is now parameterless. All test-injection constructors that referenced IDomainValidatorFactory were demoted to internal (the test projects reach them via [InternalsVisibleTo]). With this change, a v3.2 gateway host's DI container no longer trips TypeLoadException at GetParameters() — there's no missing-type reference on any constructor signature it walks.
    • Added a public method SetDomainValidatorFactory(object factory) so v3.3+ gateway hosts (or any host that can resolve the factory from their own service container) can wire up DCV after construction. The parameter type is object so the method's own signature stays loadable on older gateways; the cast to IDomainValidatorFactory happens inside the JIT-lazy method body.
    • All DCV code paths null-guard the factory, so plugin behaviour on v3.2 hosts is: load cleanly, enroll / sync / revoke normally, and silently skip DCV (each pending order is returned in EXTERNALVALIDATION state for the gateway to pick up on the next sync — exactly the same shape as a 'pending approval' result today).
    • New regression test CERTInextCAPluginPublicSurfaceTests walks public constructors via reflection and asserts none of their parameters reference IDomainValidatorFactory, IDomainValidator, or IDomainValidatorConfigProvider. Any future PR that puts a v3.3-only type back on a public constructor will fail this test instead of crashing live plugin load.

    What was NOT changed

    • integration-manifest.json#gateway_framework remains 24.2.0 and the README's compatibility line remains '24.2.0 and later'. Both are now accurate post-fix (previously they were aspirational — the plugin would not have loaded on a v3.2 host).
    • The Keyfactor.AnyGateway.IAnyCAPlugin NuGet pin remains at the v3.3 prerelease so the plugin can still use IDomainValidatorFactory internally on hosts that supply it.

    Verification

    • Local build clean against both net8.0 and net10.0 TFMs.
    • 142/142 unit tests pass (5 new public-surface tests + 137 existing).
    • Live verification against a 25.4.0 gateway is the next step; can be picked up in the same lab orchestrator pipeline that surfaced this issue.
  3. spbsoluble commented on May 22, 2026

    @spbsoluble
    CollaboratorAuthor

    Reporting back from a live re-test on gateway 25.5.0 (ships Keyfactor.AnyGateway.IAnyCAPlugin v3.2.0.0) against the post-fix build that includes ce8e02d. Initialize loads cleanly and CA registration works, but the same TypeLoadException on IDomainValidatorFactory still fires when the gateway calls Initialize during enrollment — i.e. PFX/CSR enrolls return HTTP 500 with the same stack trace shape we saw before the fix.

    Root cause that PR #2 didn't cover

    CERTInextCAPlugin.cs line 48 still declares the field type as the missing type:

    private volatile IDomainValidatorFactory _domainValidatorFactory;

    .NET JIT resolves the types of every instance field when the containing class is first instantiated — independent of whether any constructor parameter or method signature mentions the type. So the runtime can fully construct the plugin via the public parameterless ctor (the PR #2 fix), but the very first time the host calls Initialize (or any other instance method), the JIT trips TypeLoadException resolving the field's declared type against the v3.2 assembly. PR #2 fixed constructor-signature loading but did not catch field-type loading.

    Live evidence

    Gateway 25.5.0 pod log (correlation id F3B7DC2F-E4E3-4554-818C-1C42DC3B1D2C):

    2026-05-22 03:11:24.0728 AnyGatewayCore.Services.EndEntityService [Info] - Getting the necessary template information for enrollment from CA certinext-caplugin.
    2026-05-22 03:11:24.0804 AnyGatewayREST.Controllers.CertificateV1Controller [Error] - Unsuccessful Request Could not load type 'Keyfactor.AnyGateway.Extensions.IDomainValidatorFactory' from assembly 'Keyfactor.AnyGateway.IAnyCAPlugin, Version=3.2.0.0, Culture=neutral, PublicKeyToken=0ed89d330114ab09'.
       at Keyfactor.Extensions.CAPlugin.CERTInext.CERTInextCAPlugin.Initialize(IAnyCAPluginConfigProvider configProvider, ICertificateDataReader certificateDataReader) in CERTInextCAPlugin.cs:line 186
       at AnyGatewayREST.Controllers.CertificateV1Controller.CertificateRequestEnroll(CertificateRequest certificateRequest) in /src/AnyGatewayREST/Controllers/CertificateV1Controller.cs:line 221
    

    Note the stack trace lands at the start of Initialize — the body never executes; the JIT fails before the first instruction runs because resolving the _domainValidatorFactory field's declared type fails. This is the well-known "TypeLoadException happens the first time a method touches the class even if the missing type isn't on that line" behavior. The line number is the entry point of Initialize, not where the actual reference lives.

    Initialization at CA-registration time works because the gateway's CA registration flow constructs the plugin and calls a different code path that doesn't require JIT-compiling Initialize (likely a metadata-only inspection path). It's only the enrollment path that requires Initialize to actually run, which is where the JIT trip happens.

    Suggested fix

    Type the field as object (matching the SetDomainValidatorFactory(object factory) parameter pattern PR #2 already established) and cast in method bodies on demand:

    // CERTInextCAPlugin.cs line 48
    private volatile object _domainValidatorFactory;   // typed as object, not IDomainValidatorFactory

    Then anywhere the field is consumed:

    var typed = _domainValidatorFactory as IDomainValidatorFactory;
    if (typed != null) { /* use typed */ }

    The cast happens inside method bodies, which the JIT compiles lazily per-method, so the missing type is only resolved on v3.3+ hosts that supply it. Same pattern that already protects SetDomainValidatorFactory.

    The existing regression test (CERTInextCAPluginPublicSurfaceTests) walks public constructors and would not catch this — it doesn't enumerate field declarations. Worth extending the reflection walk to assert no instance field's declared type references IDomainValidatorFactory / IDomainValidator / IDomainValidatorConfigProvider either.

    Environment

    • Plugin DLL: locally built today from current main (sha256 b9f470b07e929f59186b6e7b44ad049d9a32f78987bf5ba0b3210b23e90a0429, 201728 bytes), verified in-pod via sha256sum /app/Extensions/certinext-caplugin/CERTInextCAPlugin.dll.
    • Gateway image: keyfactor.jfrog.io/kyfuseng-release/command/anygateway-rest:25.5.0 (ships Keyfactor.AnyGateway.IAnyCAPlugin v3.2.0.0).
    • Discovered via the same kfclab lab orchestrator pipeline that surfaced the original issue; the gateway side of the lab now registers 8 CERTInext templates and the CA is registered in Command, so the test fixture is end-to-end and reproducible.
  4. spbsoluble commented on May 22, 2026

    @spbsoluble
    CollaboratorAuthor

    Field-type follow-up fix landed in commit 054b2c3 on PR #2. Your diagnosis was correct — constructor-signature reflection was the first JIT trip, instance-field declared-type resolution is the second.

    What changed

    • CERTInextCAPlugin.cs:48 — _domainValidatorFactory field re-typed from volatile IDomainValidatorFactory to volatile object. The CLR no longer touches the v3.3-only type during class-load.
    • CERTInextCAPlugin.cs:63 — new private property DomainValidatorFactory returns _domainValidatorFactory as IDomainValidatorFactory. The cast is inside the property body so the JIT defers type resolution to the moment the property actually runs (on a v3.2 host where _domainValidatorFactory is null, the gating null-check short-circuits before the property is ever invoked, so the missing type is never touched).
    • Null guards (_domainValidatorFactory != null / == null) stay as direct field reads — comparing object to null doesn't touch the v3.3 type at all.
    • The dead-code nested helper DomainValidatorConfigProvider : IDomainValidatorConfigProvider was deleted. It was declared but never instantiated, and a nested type whose base list references a missing assembly type is itself a class-load hazard.

    Regression coverage you asked for

    CERTInextCAPluginPublicSurfaceTests (the reflection guard from the first pass) gained three new tests beyond the original constructor-signature walk:

    • NoInstanceField_DeclaredTypeReferencesV3Point3OnlyTypes — walks every instance field on CERTInextCAPlugin and asserts none is declared as IDomainValidatorFactory / IDomainValidator / IDomainValidatorConfigProvider. This is the exact reflection the JIT does at class-load.
    • NoNestedType_ImplementsV3Point3OnlyInterface — walks nested-type interface lists.
    • NoPublicMethod_SignatureReferencesV3Point3OnlyTypes — walks return and parameter types of public methods.

    Any future regression in any of those four reflection surfaces will fail at dotnet test instead of crashing live plugin load.

    Verification

    • Release build clean (0 warnings, 0 errors) on both net8.0 and net10.0.
    • 156/156 unit tests pass (8 in CERTInextCAPluginPublicSurfaceTests after the additions).
    • Live DCV-on enroll still completes end-to-end against the v3.3 sandbox we test against, so the DCV hot path still works where the factory IS available.
    • Local Release DLL: CERTInext/bin/Release/{net8.0,net10.0}/CERTInextCAPlugin.dll. The net8.0 is the one to drop into a 25.5.0 host.

    Ready for another lab re-test when you have a window. If Initialize still trips, please grab the stack trace + the exact DLL sha256 you're loading and I'll dig further.

  5. spbsoluble commented on May 22, 2026

    @spbsoluble
    CollaboratorAuthor

    Confirmed working in the lab on gateway 25.5.0 (IAnyCAPlugin v3.2.0.0) — plugin loads, registers, enrolls, and runs end-to-end. Closing. Thanks for the precise repro + JIT trace; it pointed directly at the field-type leak that the first pass missed.

  6. added 2 commits that reference this issue on May 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions