ADR-043: Plugin bundle admission without the platform crypto provider

Status: Accepted Date: 2026-11-04 Amends: ADR-030 (region-aware, per-plugin signing and the runtime-bundle.v2 envelope) Related ADRs: ADR-028 (verification-root lifecycle, the source of the root statuses)

Read this before changing how a host without the platform crypto provider admits a plugin bundle, before adding an algorithm to the standard bundle verifier, or before moving a type between the plugin loader and its Stella-side assembly.

Context

ADR-030 made runtime-bundle.v2 the plugin bundle envelope: a manifest with a signatures array of (algorithm, keyId, provider, profile, signaturePath) entries, detached signatures beside the assembly, and verification against a signing-root set in which any active or retiring root admits. Its verifier resolves each entry through the platform crypto provider registry, which carries the regional algorithms (for example SM2) and needs the profile-aware StellaOps.Cryptography library.

The plugin loader StellaOps.Plugin is also used by hosts without the platform crypto provider: a Router gateway host composed on StellaOps.Router.Gateway.Hosting, and other hosts that load transports, stores or backends from a folder. Such a host cannot reference the profile-aware library, the Stella Ops plugin contracts or the platform’s boot-pull signing roots, and still has to admit signed bundles fail-closed.

Decision

1. One envelope: runtime-bundle.v2

The loader admits runtime-bundle.v2 bundles through SignedRuntimePluginAdmission, unchanged from ADR-030: manifest binding (the id equals the bundle directory name; module, contract version and the required capability match what the host asks for), the assembly’s SHA-256 against the manifest, the escape-safe signature path (SignedRuntimePluginAdmission.ResolveBundleFile, public so a verifier in another assembly applies the same guard), then the signature verifier. No second envelope and no second admission chain exist.

2. RS256 and ES256 on .NET cryptography

StandardBundlePluginSignatureVerifier verifies the signature entries on the dependency-free pair StellaOps.Cryptography.Abstractions and StellaOps.Cryptography.DefaultProvider, which wrap the .NET cryptography classes:

The detached signature file is base64 text over the assembly bytes. Any entry that validates admits the bundle. An entry with another algorithm fails in this verifier (“only RS256 and ES256 are”). A runtime-bundle.v1 manifest normalizes to the legacy single entry and verifies only when the host configures a trust-root file for it.

3. A local trust-root directory

The keys come from LocalDirectoryPluginSigningRootSource: a directory laid out <root>/<profile>/<keyId>.pub (.pem accepted), one PEM SubjectPublicKeyInfo public key per file, the file name being the keyId of the signature entry. Every key on disk counts as active; the operator curates the directory and removes a revoked key. An absent directory or an empty set admits nothing. No network call and no platform service is involved, so the source works offline and in an air-gapped installation.

4. Regional entries stay on the Stella side

Region-aware verification (SM2 and the other profile algorithms through the crypto provider registry), the platform boot-pull signing roots, cosign verification, release manifest signing, the configured verifier selector and the host probe report live in StellaOps.Plugin.Regional (src/Plugin/), which only Stella Ops hosts reference. StellaOps.Plugin references StellaOps.DependencyInjection and the dependency-free cryptography pair, and grants its internals to its own tests only; StellaOps.Plugin.Regional uses its public API.

One bundle can carry a standard entry (RS256 or ES256) and a regional entry. A host without the platform crypto provider admits it on the standard entry; a Stella Ops host admits it on either.

5. The package DLL is also the folder plugin

A transport, store or backend assembly carries its own plugin entry, so the DLL that a NuGet package delivers is the same file a host loads from a signed bundle. A bundle signs one assembly; a second library inside the bundle directory would load unverified, so a host refuses it. A component whose parts are two assemblies (for example the client and server halves of a Router transport) ships as two bundles, and a reference between admitted assemblies resolves to the admitted one.

Consequences

First host

StellaOps.Router.Gateway.Hosting reads Gateway:TransportPlugins:Directory (one directory per bundle; module router, capability router:transport, contract runtime-bundle.v2), Gateway:TransportPlugins:TrustRootDirectory and the optional Gateway:TransportPlugins:Profile. A refused bundle stops the host. Configuration and the bundle layout: Router gateway host.

Alternatives considered

References