Capability Grant Hardening
This article documents provider-neutral capability grant validation, proof handling, and bounded-use checks for AsiBackbone.
AsiBackbone can model short-lived, scoped grants for governed execution, but it does not replace host authentication, host authorization, resource authorization, or external execution controls.
Important
A capability grant is not broad authority. It should be short-lived, scoped, bound to policy and acknowledgment context when needed, and checked at the execution boundary before any consequential action proceeds.
Grant metadata
CapabilityGrant models the metadata a host can protect, persist, and validate:
| Field | Purpose |
|---|---|
| Token ID | Stable grant identifier for validation and bounded-use checks. |
| Issuer | Host or service that created the grant. |
| Audience | Intended execution gateway, service, or host boundary. |
| Subject ID | Optional authenticated principal the issuer authorizes. It has effect only when the relying party compares it with the current subject. |
| Operation name | Optional operation the issuer authorizes. It has effect only when the relying party compares it with the requested operation. |
| Scopes | Least-privilege actions allowed by the grant. |
| Issued UTC | Timestamp when the grant was created. |
| Not-before UTC | Optional timestamp before which the grant is not valid. |
| Expires UTC | Timestamp after which the grant is no longer valid. |
| Policy version/hash | Binds the grant to the policy context that produced it. |
| Acknowledgment/handshake reference | Binds follow-on execution to the approval or acknowledgment flow that authorized it. |
| Gateway/resource binding | Limits the grant to a specific gateway or target resource when supplied. |
The grant model is not a wire format. Hosts decide whether they serialize it as JSON, wrap it in a signed envelope, store it server-side, or project it into another provider-owned format.
Canonical payload for a signed grant
Use CanonicalPayloadBuilder.ForCapabilityGrant to build the payload a grant is signed over:
CanonicalPayload payload = CanonicalPayloadBuilder.ForCapabilityGrant(grant);
CanonicalPayloadHash hash = CanonicalPayloadHasher.ComputeHash(payload);
The builder covers every field the grant carries, so the hash binds the whole grant. A hand-rolled payload that signs only a few fields leaves the rest outside the proof while the validator still enforces them, which means a value that was changed after signing can still pass validation. Scopes are normalized to a sorted, de-duplicated, ordinal set, so grants that differ only in scope ordering hash identically; a different scope set does not.
Important
Grant metadata is filtered through CanonicalPayloadOptions.AllowsMetadataKey, and the default allow-list is empty. With default options no grant metadata is included in the proof. This keeps unbounded and potentially sensitive host data out of hashed payloads, but it also means security-relevant data placed in grant metadata is unbound until its key is allow-listed:
CanonicalPayloadOptions options = CanonicalPayloadOptions.Create(metadataKeyAllowList: ["region"]);
CanonicalPayload payload = CanonicalPayloadBuilder.ForCapabilityGrant(grant, options);
Use the same options wherever the payload is rebuilt, or the hashes will not agree.
Validation profiles
CapabilityGrantValidationOptions provides explicit profiles so callers can communicate whether validation is occurring at a consequential execution boundary or is intentionally limited to metadata and time-bound checks.
| Profile | Subject/operation binding | Proof | Acknowledgment reference | Bounded-use/replay check | Intended use |
|---|---|---|---|---|---|
CreateBoundExecutionBoundary(...) |
Subject required; operation optional | Required | Optional; caller can require it | Required by default; caller must explicitly disable it when another boundary owns replay/use enforcement | Operational gateways and other consequential execution boundaries |
CreateMetadataValidation(...) |
Optional | Not performed | Optional; caller can require it | Not performed | Structural, temporal, policy, scope, and binding validation where proof/use enforcement is intentionally out of scope |
Create(...) |
Optional | Configurable; default is off | Configurable; default is off | Configurable; default is off | Fully configurable validation path |
Caution
Calling CapabilityGrantValidator.ValidateAsync(signedGrant) without explicit options preserves the existing 3.x behavior. It validates metadata and temporal constraints using CapabilityGrantValidationOptions.Create() defaults, but it does not verify the signed artifact proof and does not perform a bounded-use/replay check. Do not treat the no-options path as execution-boundary validation.
The execution-boundary profile requires CapabilityGrantBindingExpectations built from the authenticated current subject
and always requires proof verification. Include the requested operation whenever the grant is intended for a specific
operation. Both
comparisons are ordinal and fail closed when the grant value is missing or differs. Bounded-use validation is enabled by
default with maxUseCount: 1, but the host can explicitly set requireUseCheck: false when replay/use enforcement is
performed atomically by another trusted execution boundary. That opt-out should be intentional and documented by the host.
Copies created through WithExpectedBindings preserve the execution profile's required subject when the replacement is
omitted or blank, so later option customization cannot silently downgrade the profile to subject-unbound validation.
The metadata-validation profile keeps expectedSubjectId and expectedOperationName optional and intentionally does not
expose proof or use-check switches. Omitting those expectations means that validation result makes no claim that the grant
belongs to the current subject or requested operation. If proof or bounded-use behavior is needed, use
CreateBoundExecutionBoundary(...) or the fully configurable Create(...) factory instead.
Validation at the execution boundary
Use CapabilityGrantValidator.ValidateAsync(...) before follow-on execution. Validation can check:
- proof through the existing signing verification seam;
- issuer and audience;
- expiration and not-before time, with optional host-selected clock-skew tolerance;
- required scopes;
- policy version and policy hash;
- acknowledgment and handshake references;
- gateway and resource bindings;
- bounded-use state through an
ICapabilityGrantUseStore.
Use the explicit execution-boundary profile for consequential execution:
CapabilityGrantValidationResult result = await CapabilityGrantValidator.ValidateAsync(
signedGrant,
CapabilityGrantValidationOptions.CreateBoundExecutionBoundary(
CapabilityGrantBindingExpectations.Create(authenticatedSubjectId, "robotics.execute"),
issuer: "policy-engine",
audience: "robotics-gateway",
scopes: ["robotics.execute"],
policyVersion: "policy-v1",
policyHash: "policy-hash",
acknowledgmentId: "ack-123",
requireAcknowledgmentReference: true,
maxUseCount: 1),
verificationService,
useStore,
cancellationToken);
Proceed only when result.ShouldAllow is true. The strict profile fails closed when proof verification is unavailable and defers when the required bounded-use store is unavailable.
Intentional metadata-only validation
Some hosts need to inspect a grant before reaching the execution boundary, for example while routing a request, validating expected scope or policy bindings, or presenting diagnostic information. Use the explicitly named metadata profile for that case:
CapabilityGrantValidationResult metadataResult = await CapabilityGrantValidator.ValidateAsync(
signedGrant,
CapabilityGrantValidationOptions.CreateMetadataValidation(
issuer: "policy-engine",
audience: "robotics-gateway",
scopes: ["robotics.execute"],
policyVersion: "policy-v1",
policyHash: "policy-hash")
.WithExpectedBindings(authenticatedSubjectId, "robotics.execute"),
cancellationToken: cancellationToken);
A successful metadata-only result means only that the configured structural and temporal checks passed. It does not establish proof authenticity, replay resistance, single-use enforcement, authentication, authorization, or permission to execute an external action.
3.x migration guidance
The explicit profiles were introduced additively and do not silently change the historical 3.x behavior.
- Existing calls to
CapabilityGrantValidationOptions.Create(...)continue to honor their current arguments and defaults. - Existing calls to
ValidateAsync(signedGrant)continue to use the legacy default options where proof, acknowledgment-reference, and bounded-use checks are disabled. - New operational-gateway and consequential-execution code should use
CreateBoundExecutionBoundary(...). - Replace existing
CreateExecutionBoundary(...)calls withCreateBoundExecutionBoundary(...)and passCapabilityGrantBindingExpectationsbuilt from the authenticated current subject as the required first argument. Include the requested operation when the issuer restricted the grant to an operation. - Code that intentionally performs only structural or temporal validation should prefer
CreateMetadataValidation(...)so the reduced validation contract is visible in code review. - Hosts migrating an existing execution boundary should supply both an
IGovernanceSignatureVerificationServiceand, when the profile keeps its default bounded-use requirement, anICapabilityGrantUseStore.
The ambiguous no-options path remains available for 3.x compatibility. A future major version may tighten or remove that path; such a change would require explicit migration guidance rather than a silent behavioral change.
Legacy CreateExecutionBoundary deprecation
CapabilityGrantValidationOptions.CreateExecutionBoundary(...) is retained in the 7.x binary surface but is not a
usable execution-boundary factory. It always throws InvalidOperationException because consequential execution now
requires an authoritative subject binding.
The retained method is marked obsolete with warning diagnostic ASIB901. The warning is intentionally not an error in
7.x: changing it to error: true would turn previously compilable source into a build failure within the same stable
major line. The method is planned for removal in 8.0, where removal is permitted as a major-version API break.
See the ASIB901 migration guide for the diagnostic reference.
Migrate by replacing CreateExecutionBoundary(...) with CreateBoundExecutionBoundary(...) and passing
CapabilityGrantBindingExpectations as the required first argument:
// Legacy: compiles with ASIB901 in 7.x, then fails closed if executed.
CapabilityGrantValidationOptions.CreateExecutionBoundary(
issuer: "policy-engine",
audience: "robotics-gateway",
scopes: ["robotics.execute"]);
// Replacement: authoritative subject binding is required.
CapabilityGrantValidationOptions.CreateBoundExecutionBoundary(
CapabilityGrantBindingExpectations.Create(
authenticatedSubjectId,
"robotics.execute"),
issuer: "policy-engine",
audience: "robotics-gateway",
scopes: ["robotics.execute"]);
CapabilityGrantBindingExpectations.Create(...) requires the authenticated current subject. Supply the requested
operation when the grant is operation-bound. Existing binaries that reference CreateExecutionBoundary(...) continue
to resolve the method in 7.x and retain its fail-closed runtime behavior; consumers should migrate source before the
next major version removes the legacy entry point.
Clock-skew tolerance
CapabilityGrantValidationOptions.AllowedClockSkew lets a host accommodate a small, bounded difference between the issuer clock and the validator clock. The default is TimeSpan.Zero, which preserves strict validation behavior.
A distributed host may opt into an explicit tolerance:
CapabilityGrantValidationOptions options = CapabilityGrantValidationOptions.CreateMetadataValidation(
validationUtc: hostClockUtc,
allowedClockSkew: TimeSpan.FromSeconds(15));
The boundary semantics are:
- A not-before timestamp is accepted when it is no more than the configured skew ahead of the validator clock. The exact skew boundary is accepted.
- An expiration timestamp is accepted only while the elapsed time since expiration is less than the configured skew. The exact expiration-skew boundary is rejected.
- With zero skew, a grant is deferred before
NotBeforeUtcand denied at or afterExpiresUtc, matching the strict default behavior.
Clock skew extends the effective acceptance window at both temporal boundaries. Keep it small, explicit, and selected by the host according to its deployment topology and risk model. It does not synchronize clocks, provide NTP infrastructure, or compensate for persistently incorrect system time. Production hosts should maintain reliable UTC clock synchronization and monitor clock drift rather than using a broad tolerance as a substitute.
Negative clock-skew values are rejected during option creation. The configured tolerance changes only the temporal checks; it does not relax proof, issuer, audience, scope, policy, acknowledgment, handshake, gateway, resource, replay, revocation, cancellation, or use-limit validation.
Failure behavior
Capability grant validation maps failures to host-facing actions from the verification policy model.
| Failure | Category | Default action |
|---|---|---|
| Missing proof when required | MissingProof |
Deny |
| Invalid proof | InvalidProof |
Deny |
| Wrong issuer or audience | WrongIssuer, WrongAudience |
Deny |
| Wrong or missing subject | SubjectMismatch |
Deny |
| Wrong or missing operation | OperationMismatch |
Deny |
| Expired grant | Expired |
Deny |
| Not yet valid | NotYetValid |
Defer |
| Required scope missing | WrongScope |
Deny |
| Policy mismatch | PolicyMismatch |
Deny |
| Missing acknowledgment reference | MissingAcknowledgmentReference |
RequireAcknowledgment |
| Acknowledgment or handshake mismatch | AcknowledgmentMismatch, HandshakeMismatch |
Deny |
| Gateway or resource mismatch | GatewayMismatch, ResourceMismatch |
Deny |
| Use limit exceeded | ReuseLimitExceeded |
Deny |
| Grant stopped or cancelled | Revoked, Cancelled |
Deny |
| Use store unavailable | ReplayStoreUnavailable |
Defer |
High-risk workflows should not fall back to broad authority when validation fails.
Bounded-use expectations
ICapabilityGrantUseStore is the provider-neutral seam for single-use or bounded-use workflows. Hosts own the production implementation because durable state, concurrency control, distributed locks, cache consistency, retention, and storage schema are deployment-specific.
Recommended use-store behavior:
Validate metadata and proof
-> check use state using grant ID
-> atomically consume one use when accepted
-> return use-limit, stopped, cancelled, or unavailable state when not accepted
For high-risk workflows, use checks should be atomic at the host storage boundary. Production use needs durable, concurrency-safe state that matches the host's transaction and retry model.
Reference in-memory use store
AsiBackbone.Storage.InMemory includes InMemoryCapabilityGrantUseStore as a reference implementation for tests, samples, and local validation.
The following sample intentionally isolates bounded-use behavior and does not represent the complete execution-boundary profile:
using AsiBackbone.Storage.InMemory.CapabilityGrants;
var useStore = new InMemoryCapabilityGrantUseStore();
CapabilityGrantValidationResult first = await CapabilityGrantValidator.ValidateAsync(
signedGrant,
CapabilityGrantValidationOptions.Create(requireUseCheck: true, maxUseCount: 1),
useStore: useStore,
cancellationToken: cancellationToken);
CapabilityGrantValidationResult replay = await CapabilityGrantValidator.ValidateAsync(
signedGrant,
CapabilityGrantValidationOptions.Create(requireUseCheck: true, maxUseCount: 1),
useStore: useStore,
cancellationToken: cancellationToken);
The first valid use is accepted. A second use of the same grant ID returns ReuseLimitExceeded with capability.use-limit-exceeded when maxUseCount is 1.
Hosts that use the builder facade can register the reference store explicitly:
services.AddAsiBackbone(builder =>
builder.UseInMemoryCapabilityGrantUseStore());
The in-memory store is thread-safe inside one process and can represent stopped and cancelled local-validation states through its public helpers. It is not durable, distributed, replicated, or production replay protection. It does not coordinate across replicas, survive process restarts, or replace a host-owned database/cache/lock strategy.
Retention horizon and clock skew
The store evicts a grant's use record once the grant has been expired for longer than EvictionGracePeriod (default five minutes), measured from the latest use time the store has observed. A grant past that horizon is refused with ReuseLimitExceeded and capability.use-retention-elapsed instead of being given a fresh count, because its earlier uses may already have been evicted.
Set EvictionGracePeriod to at least the largest AllowedClockSkew any validator uses with the store:
TimeSpan skew = TimeSpan.FromMinutes(2);
var useStore = new InMemoryCapabilityGrantUseStore { EvictionGracePeriod = skew };
If the grace period is shorter than the skew, grants that are expired but still inside the skew are denied rather than accepted. That is fail-closed, but it rejects uses the validator would otherwise allow. Durable implementations of ICapabilityGrantUseStore should apply the same rule: never discard a use record while the grant it describes can still pass validation, and refuse a grant whose record may have been discarded.
Stopping and cancelling grants
Use records are keyed by issuer and token ID. StopGrant(issuer, grantId) and CancelGrant(issuer, grantId) affect only that issuer's grant. The identifier-only overloads StopGrant(grantId) and CancelGrant(grantId) affect every issuer's grant that uses the identifier.
Core boundary
Core provides metadata, validation result categories, bounded-use interfaces, and signing verification integration. Core does not provide:
- a bearer-token format;
- host authentication or authorization;
- automatic proof issuance;
- durable replay storage;
- distributed locking;
- external system execution;
- legal or compliance guarantees.
Use safe wording such as "the grant was validated for this execution context." Avoid wording such as "the grant replaces authorization" or "single-use is guaranteed" unless the host store provides that guarantee under documented assumptions.