Skip to content
bitzorcas
中EN

Guide

Online License Issuance and Distribution

Configure the vendor control plane, isolated signer, and Azure KMS/HSM; operate four-eyes approval, asynchronous issuance, download, distribution, and revocation from the UI.

Last updated

The web application can operate the online issuance workflow, but the browser does not perform cryptographic signing. It calls authorized control-plane APIs. Private keys remain in Azure Key Vault or Managed HSM, and the isolated BitzOrcas.LicenseSigner asks KMS/HSM to produce ES256 signatures.

Implemented boundary

Authorized admin UI

LicenseManagement control-plane API

Requests, reviews, operation leases

Background signing worker

Isolated LicenseSigner

Azure Key Vault / Managed HSM

Durable idempotency receipts

Signed envelope + SHA-256 download

Online issuance currently includes:

  • UI creation, approval/rejection, issue, status, download, and revoke actions;
  • a license-independent bootstrap surface for login, forced password change, password recovery, permission-filtered navigation, and the authorized LicenseManagement control plane;
  • durable asynchronous calls from the API to an isolated signer;
  • version-pinned Azure key URIs;
  • independent public-key verification by the API, including a field-by-field check that only signature changed;
  • idempotent recovery across network and process failures.

It does not include anonymous customer self-service issuance, keys in the browser, direct browser-to-KMS access, or a public customer lease-download service. Operators currently download the formal envelope and distribute it through a controlled channel. Runtime can also consume a separately implemented online ILicenseLeaseSource adapter.

1. Prepare the Azure key

Create an immutable key version for each rotation and retain:

ItemExampleSecret
KeyIdkey-2026-01No; stable protocol id
Versioned key URIhttps://vault.managedhsm.azure.net/keys/license-key/<version>No, but controlled
ES256 public-key PEM-----BEGIN PUBLIC KEY-----…No; distributed to verifiers

The URI must include a concrete version, never implicit latest. Grant the signer identity only the metadata and sign operations it needs; private-key export is not required. Deliver the API public key through an independent trusted path rather than treating a KMS response as the sole trust source.

For the first local P-256 ES256 key, use Azure CLI:

Terminal window
az login
az keyvault key create \
--vault-name <vault-name> \
--name bitzorcas-runtime-license \
--kty EC \
--curve P-256 \
--ops sign verify
# Retain the complete versioned kid.
az keyvault key show \
--vault-name <vault-name> \
--name bitzorcas-runtime-license \
--query key.kid \
--output tsv
# <version> is the final segment of the preceding kid.
az keyvault key download \
--vault-name <vault-name> \
--name bitzorcas-runtime-license \
--version <version> \
--encoding PEM \
--file bitzorcas-runtime-license-public.pem

Prefer Azure RBAC for the Key Vault data plane. A local Azure CLI identity can receive Key Vault Crypto User; production should use a dedicated managed identity. Keep key creation/rotation authority separate from routine signing authority. See az keyvault key and Key Vault authentication.

2. Configure local AppHost

Complete the SQL Server, RabbitMQ, Redis, JWT, and AppHost parameters in local infrastructure. Then store development signing configuration under AppHost user secrets:

Terminal window
# Pin the protocol KeyId and immutable KMS key version.
dotnet user-secrets set \
"LicenseManagement:Development:KeyId" \
"key-2026-01" \
--project src/Hosts/BitzOrcas.AppHost
dotnet user-secrets set \
"LicenseManagement:Development:VersionedKeyUri" \
"https://<vault>.managedhsm.azure.net/keys/<name>/<version>" \
--project src/Hosts/BitzOrcas.AppHost
# Configure the matching public key independently, then enable the worker.
dotnet user-secrets set \
"LicenseManagement:Development:TrustedPublicKeyPem" \
"<ES256 public key PEM>" \
--project src/Hosts/BitzOrcas.AppHost
dotnet user-secrets set \
"LicenseManagement:Development:SigningWorkerEnabled" \
"true" \
--project src/Hosts/BitzOrcas.AppHost

To override the development control-plane tenant:

Terminal window
# Production must use a dedicated vendor control-plane tenant.
dotnet user-secrets set \
"LicenseManagement:Development:ControlPlaneTenantId" \
"1000001" \
--project src/Hosts/BitzOrcas.AppHost

AppHost generates and persists the service credential shared by API and signer, so developers do not copy a token. DefaultAzureCredential disables interactive-browser fallback. Local development may use an authenticated Azure CLI/IDE identity; production uses workload identity.

3. Start and inspect

Terminal window
scripts/local/bootstrap.sh aspire
scripts/local/doctor.sh aspire
dotnet run --project src/Hosts/BitzOrcas.AppHost

doctor.sh aspire prints no key or password material. It reports whether the signing side is missing the versioned key URI, trusted public key, or worker enablement, and whether the local DeploymentId and Runtime License Envelope exist. Missing signing configuration keeps online issuance fail-closed; missing the Envelope still permits community.small.v1 while the deployment has at most 30 users. Ordinary business operations remain denied when that policy is not selected, its authoritative meter is unavailable, or the deployment is over capacity.

For an empty Development database that needs the admin and operator four-eyes actors, replace the start command with explicit demo seeding:

Terminal window
BITZORCAS_ASPIRE_SEED_DEMO=true dotnet run --project src/Hosts/BitzOrcas.AppHost

The bootstrap writes the local-only 10-character initial-password hint to the gitignored .bitzorcas/demo-credentials with mode 0600 when the secret is missing. Demo accounts do not require a first-login password change. Restarting AppHost does not rotate that secret. To replace a legacy 44-character value, follow Demo data and seed reference. Plaintext reaches only the one-shot schema initializer and is converted to BCrypt before storage; API and JobHost never receive it. During this explicit AppHost demo seed only, higher-priority blank hash settings mask stale USER:*:PASSWORD_HASH values that may remain in API user secrets. The original secrets are not deleted, and non-Aspire seed paths still prefer precomputed hashes.

The user-role CSV keeps readable UserName input. Before authorization rows are written, the Identity adapter resolves each name to its immutable user id and validates all user and role references. Any invalid reference rejects the whole step instead of leaving partial bindings.

AppHost initializes the business and Quartz schemas before API and JobHost. Verify:

  1. schema-initializer and quartz-schema-initializer complete successfully.
  2. api, jobhost, and license-signer are Running.
  3. A user with license-management.runtime-license.view can inspect signer readiness.
  4. Readiness is healthy before an issue action.

/health/live proves only that the signer process is alive. /health/ready also checks configuration, KMS, and receipt storage and requires the service bearer. Use the control-plane signer-status view rather than putting that bearer in a browser or shell history.

4. Run four-eyes issuance in the UI

Management route:

/license-management/runtime-licenses

This route is implemented in frontend/apps/app; it is not merely a planned screen. Complete the forced password change before entering it. Without a Runtime License, unauthenticated calls should still fail as Authentication.Required, rather than being intercepted first by Licensing.Runtime.Unavailable.

Explicit Development demo seeding binds the control plane to tenant 1000001:

  • admin / root-admin: create, issue, download, and revoke;
  • operator: view, approve, and reject.

End-to-end flow:

  1. admin creates the request.
  2. operator approves it; requester and reviewer must differ.
  3. admin triggers issuance.
  4. The UI observes Pending → Processing → Completed.
  5. After status becomes Issued, download the envelope and compare its displayed SHA-256.
  6. Deliver it through a controlled channel.

A rejected request becomes immutable Rejected. Create a new request rather than editing the old record.

5. Fill the request

FieldRule
CustomerIdStable customer id, not the control-plane tenant
ProductIdPresent in signer AllowedProductIds
Edition / FeaturesMatch the contract and delivered Profile
VersionRangeExplicit supported version range
TenancyModes / EnvironmentsOnly the authorized scope
DeploymentLimit1 through 100000
NotBefore / ExpiresAtBusiness-validity window
OfflineUntil / GraceUntilNotBefore ≤ OfflineUntil ≤ GraceUntil
DeploymentIdPersistent customer deployment identity, never Pod/MAC/CPU
KeyIdExact match for the versioned KMS URI and public-key ring

Protocol v1 is for dedicated deployments. Protocol v2 currently admits only community.web.v1; the UI fixes its Profile, Composition, and shared DeploymentId, while the API remains the final validator.

FieldValue
ProductIdbitzorcas-modern
EditionEnterprise
Featuresframework.core, framework.aspnetcore, framework.infrastructure, workflow.runtime
VersionRange[1.0.0,2.0.0)
TenancyModesmulti-tenant
Environmentsdevelopment
DeploymentLimit1
DeploymentIdContents of .bitzorcas/license/deployment-id
KeyIdThe stable AppHost alias, for example key-2026-01
ProtocolVersion1

The time window must satisfy at least NotBefore ≤ ExpiresAt and NotBefore ≤ OfflineUntil ≤ GraceUntil, while covering the intended debugging period. The submitted payload is immutable; create a new request if any value is wrong.

Obtain the customer DeploymentId

The customer first configures a persistent Licensing:Runtime:DeploymentIdentityPath. Runtime atomically creates a 32-character lowercase GUID the first time it accesses this store. The customer transfers that id to the issuer through a controlled channel and retains the file on persistent storage.

Deleting or regenerating it after issuance causes a deployment-identity mismatch and an Invalid license.

6. Background signing and retry

The issue button does not synchronously wait for KMS. The API freezes request and idempotency facts in a short transaction. The worker then:

  1. claims a database lease;
  2. calls the signer outside the transaction;
  3. obtains or recovers a durable signing receipt;
  4. reverifies the signature and full payload;
  5. commits the envelope in another transaction.

Transient failures use bounded backoff. Eight unsuccessful attempts or corrupted durable payload move the operation to Failed with a safe stable error code. Repair KMS, network, storage, or configuration, then explicitly requeue from the control plane. Do not edit database status.

7. Production configuration

API/control plane:

LicenseManagement:ControlPlane:TenantId=<dedicated vendor tenant>
LicenseManagement:Signing:BaseUrl=https://license-signer.internal
LicenseManagement:Signing:SignPath=/v1/licenses/sign
LicenseManagement:Signing:ReadinessPath=/health/ready
LicenseManagement:Signing:Provider=production-kms
LicenseManagement:Signing:TimeoutSeconds=10
LicenseManagement:Signing:TrustedPublicKeys:<key-id>=<public-key PEM>
LicenseManagement:Signing:BearerToken=<short-lived service credential>
LicenseManagement:SigningWorker:Enabled=true
LicenseManagement:SigningWorker:PollIntervalSeconds=5
LicenseManagement:SigningWorker:BatchSize=10
LicenseManagement:SigningWorker:LeaseSeconds=90

Isolated signer:

LicenseSigner:AllowedProductIds:0=bitzorcas-modern
LicenseSigner:Authentication:BearerToken=<same current value as API>
LicenseSigner:Authentication:PreviousBearerToken=<rotation window only>
LicenseSigner:AzureKeyVault:Keys:<key-id>=https://<vault>/keys/<name>/<version>
LicenseSigner:ReceiptStore:Directory=/var/lib/bitzorcas/license-signing-receipts

Production requirements:

  • API, signer, and KMS use a controlled private network;
  • the control plane uses a dedicated tenant, not a business tenant;
  • UI permissions separate create, approve, issue, export, and revoke;
  • receipt storage is a dedicated durable volume; replicas share atomic-create storage or the signer stays single-replica;
  • service credentials, workload identity, and KMS auditing are managed by security operations;
  • private keys, bearers, and raw signing requests never enter logs.

8. Rotate credentials and keys

Service bearer:

  1. Configure the signer with the new BearerToken and old value in PreviousBearerToken.
  2. Move every API instance to the new value.
  3. Confirm the old value is unused.
  4. Remove PreviousBearerToken.

Signing key:

  1. Create a new immutable version and KeyId.
  2. Distribute the new public key to API and Runtime first.
  3. Confirm every target trusts it.
  4. Start issuing with the new KeyId.
  5. Remove the old public key only after reissue or the contractual window ends.

9. Download and activate for a customer

The download contains a suggested file name, JSON envelope, and SHA-256. Deliver it through an authenticated portal, enterprise file exchange, or equivalent controlled channel. Do not paste it into a public ticket.

Minimum customer Runtime configuration:

Licensing:Runtime:Enabled=true
Licensing:Runtime:ProductId=bitzorcas-modern
Licensing:Runtime:ProductVersion=<deployed version>
Licensing:Runtime:Environment=<licensed environment>
Licensing:Runtime:TenancyMode=<licensed tenancy mode>
Licensing:Runtime:DeploymentIdentityPath=/var/lib/bitzorcas/license/deployment-id
Licensing:Runtime:CachePath=/var/lib/bitzorcas/license/runtime-license.json
Licensing:Runtime:OfflineLicensePath=/run/secrets/bitzorcas-license.json
Licensing:Runtime:TrustedPublicKeys:<key-id>=<public-key PEM>

Inspect /health/license after startup. The status becomes Valid only when signature, runtime context, DeploymentId, and time windows all match.

10. Revoke

Only a principal with license-management.runtime-license.revoke may revoke. After a reason and confirmation, the control plane asks the same KMS to sign a verifiable Revoked=true envelope. Deliver that result to the target Runtime or online lease source; readiness becomes Unhealthy and business execution fails closed.

Deleting a database row or customer file is not verifiable revocation.

Troubleshooting

Status/errorResponse
license-signer.not-configuredCheck AllowedProductIds, bearer, versioned URI, receipt directory
provider-unavailableCheck workload identity, KMS network, and get/sign permission
receipt-store-unavailableCheck dedicated volume, access, and replica sharing
invalid-requestCheck KeyId, canonical collections, timeline, policy, deployment limit
idempotency-conflictOne key bound different payloads; stop and investigate the caller
Long RetryScheduledCheck readiness, worker switch, and next-attempt time
Runtime InvalidCompare public key, context, DeploymentId, and file integrity

See also

100%

Scroll or use controls to zoom · drag when enlarged · double-click for 100% / 200%