Signing keys by purpose

Feature code requests a signing key by canonical tenant UUID, signing purpose and active cryptography profile. PurposeSigningKeyResolver in StellaOps.Cryptography.CredentialStore owns the lookup and returns an ICryptoSigner. Callers do not select a private-key format or parse key files.

For example, compact vulnerability database manifests use SignaturePurpose.EvidenceBundle. The library maps that purpose to the stable owner key crypto/signing/evidence-bundle/v1; the existing secret store provides tenant isolation. This path is a custody identifier, not a configurable command argument. Provider handles and private-key encodings remain inside the cryptography libraries.

var request = new PurposeSigningKeyRequest(
    tenantUuid, SignaturePurpose.EvidenceBundle, activeProfile);
var key = await resolver.ResolveAsync(request, cancellationToken);
var signature = await key.Signer.SignAsync(paeBytes, cancellationToken);
var verified = await key.Signer.VerifyAsync(paeBytes, signature, cancellationToken);
var publicJwk = key.Signer.ExportPublicJsonWebKey();

Hosts supply their existing ICryptoProviderRegistry and ISecretProvider. Remote operator clients can instead implement the read-only IPurposeSigningKeySource port through the secret owner’s authenticated API. The library supplies the reference and tenant context to that port. The adapter must preserve both; it must not query another service’s database.

SignatureProviderSelection selects the algorithm from the active profile. The registry selects the provider according to its configured preference order or an explicit deployment provider binding. The enrolled record must match that provider, algorithm, tenant and purpose. An unavailable provider, conflicting key identifier, malformed record, missing key or invalid validity period stops the operation. Resolution never generates a replacement or changes stored custody.

Enrollment and verification

Enrollment is a separate operator action through the existing secret-management workflow. Read the exact purpose path first and reuse a valid existing record. An existing invalid record requires investigation; creating a second record is not recovery.

When the purpose is absent, an operator can create its record with PurposeSigningKeyMaterial.Generate. This calls the selected provider’s optional ICryptoSigningKeyGenerator capability. The built-in software provider supports its existing ES256 algorithm. Providers without that capability require their own custody enrollment; EncodeProviderReference records a provider-held key without exporting private material. There is no fallback from an unavailable regional provider to the software provider.

Seal the returned bytes immediately through the existing secret-provider workflow at the library’s owner key, then clear the plaintext buffer. Never log the record or write it to an unencrypted file. Conduct enrollment with one operator for the purpose and check absence again immediately before the seal; the existing generic seal operation does not provide an atomic create-if-absent contract. Normal exports perform lookup only.

After enrollment, resolve through a fresh registry and the durable store. Sign and verify a challenge, reject a modified challenge, and repeat resolution to confirm the same key identifier. Publish only public verification material and the non-secret purpose/provider/key identifier. Public material alone is not a trust decision: distribute its trust binding through the release’s established channel.

The adjacent PurposeSigningKeyResolverTests exercise signature verification, tenant and purpose isolation, provider selection, key conflicts, validity, malformed material, and refusal to create keys during lookup.

Installation runtime-plugin keys

The deployed runtime-bundle.v1 detached-signature contract requires RS256. Its installation/module key is resolved inside StellaOps.Cryptography by RuntimePluginSigningKeyResolver, using the fixed runtime-plugin-artifact purpose and the configured custody root. The returned handle signs and verifies the actual artifact bytes. The public-key SHA-256 identifier identifies the key; it is not a signature. This contract-specific resolver does not select tenant profiles or replace PurposeSigningKeyResolver for tenant evidence.

The existing plugin SigningTool exposes sign-runtime, export-runtime-public and the separate operator command enroll-runtime-key. Each requires --module and --key-root; signing also requires --assembly and --out, and public export requires --out. Enrollment creates RSA-3072 through the cryptography library and refuses an existing key. Lookup never enrolls or replaces a key. The library owns the existing module custody encoding; callers do not choose a key format. Enrollment writes atomically, with owner-only Unix permissions; Windows operators restrict the custody directory ACL before enrollment.

Keep custody under the installation’s durable .local-secrets/ directory, excluded from Git and Docker contexts, and preserve it during cleanup. Pass the same configured root to the packager’s -OfflineDevSigningKeyRoot when using -UseOfflineDevSigner. Existing installations can continue resolving their configured durable module directory. If a key is lost, enroll only with explicit operator authority, re-sign the affected bundle cohort, verify admission with the new public trust root, and apply the bundle/trust change in one controlled window. Merely exporting a new public key does not rotate a running host.

Protocol-defined streaming digests

Artifact wire contracts that explicitly name SHA-256 must keep that digest across profiles. Use CryptoHashFactory.CreateIncremental(HashAlgorithms.Sha256) and ICryptoIncrementalHash.AppendData while reading or writing chunks; finish with GetHashAndReset. The factory also accepts SHA-384 and SHA-512 and refuses missing or unsupported algorithms. It buffers hash state rather than the full artifact. For a complete input stream, use ICryptoHash.ComputeHashHexAsync with the contract’s explicit algorithm. Both paths keep primitive calls inside the central library; ordinary purpose-selected hashing retains its existing profile routing.