7. Architecture of the Proposed Provider

This chapter specifies the overall shape of the provider: its module structure, its initialisation, the context objects it maintains, and the conventions the per-algorithm chapters that follow will assume. Requirements F8, F9 and N3–N6 are addressed here.

7.1 Module structure

The provider is a single shared object, conventionally installed beside the other modules:

/usr/local/lib/ossl-modules/tlsext.so

Internally it is organised by operation, with one translation unit per algorithm class and a small core:

tlsext/
  provider.c      OSSL_provider_init, query dispatch, provider params
  ctx.c           provider context, configuration, delegate handles
  cipher_aead.c   block cipher in AEAD mode          (Chapter 8)
  digest.c        hash function                       (Chapter 9)
  keymgmt_ec.c    key management for the EC group     (Chapter 10)
  keyexch_ec.c    key agreement                       (Chapter 10)
  signature.c     signature operations                (Chapter 11)
  capabilities.c  TLS-GROUP and TLS-SIGALG answers    (Chapters 10, 11)
  params.c        shared OSSL_PARAM helpers

The separation matters for more than tidiness. Because the core's query function dispatches on operation identifier, a provider that offers several operations is effectively several independent implementations sharing one initialisation. Keeping them in separate units keeps the dispatch tables next to the functions they name and makes it possible to build a reduced variant — digest only, say — by omitting a unit and its table entry.

7.2 Initialisation

The initialisation function performs four tasks in order: it records the core handle, it walks the incoming dispatch table to find the core functions it needs, it allocates and populates the provider context, and it returns its own dispatch table.

int OSSL_provider_init(const OSSL_CORE_HANDLE *handle,
                       const OSSL_DISPATCH *in,
                       const OSSL_DISPATCH **out,
                       void **provctx)
{
    PROV_CTX *ctx;

    if ((ctx = prov_ctx_new(handle, in)) == NULL)
        return 0;
    *provctx = ctx;
    *out = provider_dispatch;
    return 1;
}

Two conventions are worth stating because they are easy to get wrong and hard to debug. The incoming table must be walked, not indexed: the core is not obliged to supply entries in any particular order, and a provider that assumes an order will read the wrong pointer on a different OpenSSL version. And the provider must tolerate the absence of optional core functions, using them only if present.

Design. The provider context holds the core handle, the resolved core function pointers, a private OSSL_LIB_CTX used for any delegated operation (§7.4), and configuration read from the provider's section of the configuration file. It is created once per activation and destroyed by the teardown entry point.

7.3 Query dispatch

The query function answers one array per operation identifier:

static const OSSL_ALGORITHM *query(void *provctx, int operation_id,
                                   int *no_cache)
{
    *no_cache = 0;
    switch (operation_id) {
    case OSSL_OP_DIGEST:       return tlsext_digests;
    case OSSL_OP_CIPHER:       return tlsext_ciphers;
    case OSSL_OP_KEYMGMT:      return tlsext_keymgmt;
    case OSSL_OP_KEYEXCH:      return tlsext_keyexch;
    case OSSL_OP_SIGNATURE:    return tlsext_signatures;
    }
    return NULL;
}

Setting no_cache to zero permits the core to cache the returned array, which is correct for static tables and is the normal case. A provider whose algorithm set varies at run time would set it to one and accept the cost.

7.4 Delegation and the private library context

An algorithm implementation may compute its result itself or delegate part of the work to another provider. Delegation is useful for a component that is not the subject of the exercise — for instance a design that wishes to study integration mechanics may delegate the underlying primitive while retaining full control of the provider-side interface. The companion paper's provider does exactly this for SHA-256.

Delegation must use a private library context, created by the provider and holding only the default provider. Fetching from the application's context would be circular: the provider would be asking the context in which it is itself registered to resolve a name it may itself answer to, and the result depends on property queries the provider does not control. A private context makes the delegate deterministic.

ctx->delegate_libctx = OSSL_LIB_CTX_new();
ctx->delegate_md = EVP_MD_fetch(ctx->delegate_libctx, "SHA2-256",
                                "provider=default");

The delegate handle is fetched once at provider initialisation and reused, satisfying N1 for the delegated path.

7.5 Context objects and lifecycle

Every operation in the provider interface follows the same lifecycle: a context is created, initialised, used, possibly duplicated, and freed. The provider supplies each step as a dispatch entry. The invariants the design maintains are:

7.6 Parameter conventions

Requirement F8 obliges every algorithm to report its parameters. The design follows one convention throughout: each algorithm supplies a gettable function returning a static descriptor array, and a get function that fills a caller-supplied array. Parameters not recognised are skipped rather than treated as errors, which is what allows a newer consumer to ask an older provider for something and proceed without it.

The parameters each algorithm class must answer are specified in the respective chapters and collected in Appendix C.

7.7 Error reporting

Requirement N6 obliges the provider to report through the core rather than to standard error. The core supplies error functions in the incoming dispatch table; the provider resolves them at initialisation and uses them for every failure that a caller could act on. A provider that prints diagnostics directly is unusable in a server, where the output goes nowhere useful and may itself constitute an information leak.

The design distinguishes three failure classes: configuration failures, raised at initialisation and causing activation to fail; parameter failures, raised when a caller supplies something the algorithm cannot accept; and operational failures, raised when a cryptographic operation cannot be completed. Only the first prevents the provider from loading.

7.8 What this chapter has fixed

The remaining design chapters may now assume: a single module with per-operation units; a provider context created at initialisation and carrying resolved core functions and a private delegate context; a query function dispatching on operation; the create/init/use/dup/free lifecycle with the invariants of §7.5; the parameter convention of §7.6; and core-mediated error reporting. Each algorithm chapter specifies only what is particular to its class.