< Summary

Information
Class: AsiBackbone.Core.CapabilityGrants.CapabilityGrantValidator
Assembly: AsiBackbone.Core
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/CapabilityGrants/CapabilityGrantValidator.cs
Line coverage
95%
Covered lines: 230
Uncovered lines: 12
Coverable lines: 242
Total lines: 418
Line coverage: 95%
Branch coverage
92%
Covered branches: 115
Total branches: 125
Branch coverage: 92%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
ValidateAsync()92.86%1414100%
ValidateProofAsync()90%1010100%
ValidateProofBinding(...)100%6676.47%
ValidateMetadata(...)100%5252100%
IsNotYetValid(...)100%44100%
IsExpired(...)100%22100%
ValidateUseAsync()73.68%191994.74%
ContainsRequiredScopes(...)100%44100%
MapVerificationCategory(...)78.57%151482.35%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/CapabilityGrants/CapabilityGrantValidator.cs

#LineLine coverage
 1using AsiBackbone.Core.Signing;
 2
 3namespace AsiBackbone.Core.CapabilityGrants;
 4
 5public static class CapabilityGrantValidator
 6{
 7    public static async ValueTask<CapabilityGrantValidationResult> ValidateAsync(
 8        SignedGovernanceArtifact<CapabilityGrant> signedGrant,
 9        CapabilityGrantValidationOptions? options = null,
 10        IGovernanceSignatureVerificationService? verificationService = null,
 11        ICapabilityGrantUseStore? useStore = null,
 12        CancellationToken cancellationToken = default)
 13    {
 7514        ArgumentNullException.ThrowIfNull(signedGrant);
 7415        cancellationToken.ThrowIfCancellationRequested();
 16
 7317        CapabilityGrant grant = signedGrant.Artifact;
 18
 19        // The previous default built permissive options: no proof, no use check, and no issuer, audience, or scope
 20        // expectations, so the simplest call was the least safe one and returned Valid for anything unexpired. Validati
 21        // now requires the caller to state what it is validating against.
 7322        if (options is null)
 23        {
 124            return CapabilityGrantValidationResult.Failed(
 125                grant,
 126                CapabilityGrantValidationCategory.Failed,
 127                VerificationPolicyAction.Deny,
 128                "capability.validation-options-required",
 129                "Capability grant validation requires explicit options describing what the grant is validated against.")
 30        }
 31
 7232        CapabilityGrantValidationOptions effectiveOptions = options;
 7233        DateTimeOffset validationUtc = (effectiveOptions.ValidationUtc ?? DateTimeOffset.UtcNow).ToUniversalTime();
 34
 7235        if (effectiveOptions.RequireProof)
 36        {
 2337            CapabilityGrantValidationResult? proofResult = await ValidateProofAsync(
 2338                signedGrant,
 2339                grant,
 2340                effectiveOptions,
 2341                verificationService,
 2342                cancellationToken).ConfigureAwait(false);
 43
 2344            if (proofResult is not null)
 45            {
 1946                return proofResult;
 47            }
 48        }
 49
 5350        CapabilityGrantValidationResult? metadataResult = ValidateMetadata(grant, effectiveOptions, validationUtc);
 51
 5352        if (metadataResult is not null)
 53        {
 2954            return metadataResult;
 55        }
 56
 2457        if (effectiveOptions.RequireUseCheck)
 58        {
 1759            CapabilityGrantValidationResult? useResult = await ValidateUseAsync(
 1760                grant,
 1761                effectiveOptions,
 1762                useStore,
 1763                validationUtc,
 1764                cancellationToken).ConfigureAwait(false);
 65
 1766            if (useResult is not null)
 67            {
 1068                return useResult;
 69            }
 70        }
 71
 1472        return CapabilityGrantValidationResult.Valid(grant);
 7373    }
 74
 75    private static async ValueTask<CapabilityGrantValidationResult?> ValidateProofAsync(
 76        SignedGovernanceArtifact<CapabilityGrant> signedGrant,
 77        CapabilityGrant grant,
 78        CapabilityGrantValidationOptions options,
 79        IGovernanceSignatureVerificationService? verificationService,
 80        CancellationToken cancellationToken)
 81    {
 2382        if (verificationService is null)
 83        {
 284            return CapabilityGrantValidationResult.Failed(
 285                grant,
 286                CapabilityGrantValidationCategory.MissingProof,
 287                VerificationPolicyAction.Deny,
 288                "capability.proof-verifier-missing",
 289                "A proof verifier is required for this validation context.");
 90        }
 91
 2192        CapabilityGrantValidationResult? bindingResult = ValidateProofBinding(signedGrant, grant, options);
 93
 2194        if (bindingResult is not null)
 95        {
 496            return bindingResult;
 97        }
 98
 1799        var verificationContext = VerificationPolicyContext.Create(
 17100            purpose: CanonicalArtifactTypes.CapabilityGrant,
 17101            expectedKeyId: options.ExpectedProofKeyId,
 17102            expectedKeyVersion: options.ExpectedProofKeyVersion,
 17103            expectedPolicyVersion: options.ExpectedProofPolicyVersion,
 17104            expectedPolicyHash: options.ExpectedProofPolicyHash,
 17105            requiredProvider: options.RequiredProofProvider,
 17106            requiredHashAlgorithm: options.RequiredProofHashAlgorithm);
 107
 17108        VerificationPolicyOutcome verificationOutcome = await GovernanceArtifactVerifier.VerifyAsync(
 17109            signedGrant,
 17110            verificationService,
 17111            context: verificationContext,
 17112            cancellationToken: cancellationToken).ConfigureAwait(false);
 113
 17114        if (verificationOutcome.ShouldAllow)
 115        {
 4116            return null;
 117        }
 118
 119        // A grant whose signature was stripped is not a grant awaiting acknowledgment. Where proof is required, absent
 120        // proof denies, rather than inviting a host that treats RequireAcknowledgment as "proceed after a click" to
 121        // continue on a grant carrying no proof at all. The default verification policy now also denies a missing
 122        // signature; this override is retained so grant validation cannot be weakened by a future default change.
 13123        VerificationPolicyAction action = verificationOutcome.Category is SignatureVerificationCategory.MissingSignature
 13124            ? VerificationPolicyAction.Deny
 13125            : verificationOutcome.Action;
 126
 13127        return CapabilityGrantValidationResult.Failed(
 13128            grant,
 13129            MapVerificationCategory(verificationOutcome.Category),
 13130            action,
 13131            verificationOutcome.FailureCode ?? "capability.proof-invalid",
 13132            verificationOutcome.FailureMessage);
 23133    }
 134
 135    /// <summary>
 136    /// Binds the signed proof to the grant being validated by rebuilding the canonical payload from the grant itself.
 137    /// </summary>
 138    /// <remarks>
 139    /// Signature verification establishes that a signature covers a hash. It does not establish that the hash describes
 140    /// grant whose fields are about to be evaluated. Rebuilding the payload from <see cref="SignedGovernanceArtifact{TA
 141    /// and comparing hashes closes that gap, and the artifact descriptors are asserted so a proof issued for another ar
 142    /// type or token identifier cannot be presented alongside this grant.
 143    /// </remarks>
 144    private static CapabilityGrantValidationResult? ValidateProofBinding(
 145        SignedGovernanceArtifact<CapabilityGrant> signedGrant,
 146        CapabilityGrant grant,
 147        CapabilityGrantValidationOptions options)
 148    {
 21149        if (!string.Equals(signedGrant.ArtifactType, CanonicalArtifactTypes.CapabilityGrant, StringComparison.Ordinal))
 150        {
 1151            return CapabilityGrantValidationResult.Failed(
 1152                grant,
 1153                CapabilityGrantValidationCategory.InvalidProof,
 1154                VerificationPolicyAction.Deny,
 1155                "capability.proof-artifact-type-mismatch",
 1156                "The signed artifact type is not a capability token grant.");
 157        }
 158
 20159        if (!string.Equals(signedGrant.ArtifactId, grant.TokenId, StringComparison.Ordinal))
 160        {
 1161            return CapabilityGrantValidationResult.Failed(
 1162                grant,
 1163                CapabilityGrantValidationCategory.InvalidProof,
 1164                VerificationPolicyAction.Deny,
 1165                "capability.proof-artifact-id-mismatch",
 1166                "The signed artifact identifier does not match the grant token identifier.");
 167        }
 168
 169        CanonicalPayloadHash recomputedHash;
 170
 171        try
 172        {
 19173            recomputedHash = CanonicalPayloadHasher.ComputeHash(
 19174                CanonicalPayloadBuilder.ForCapabilityGrant(grant, options.ProofPayloadOptions),
 19175                signedGrant.HashAlgorithm);
 19176        }
 0177        catch (NotSupportedException)
 178        {
 0179            return CapabilityGrantValidationResult.Failed(
 0180                grant,
 0181                CapabilityGrantValidationCategory.InvalidProof,
 0182                VerificationPolicyAction.Deny,
 0183                "capability.proof-hash-algorithm-unsupported",
 0184                "The grant canonical payload cannot be rebuilt with the built-in hasher, so the proof is not bound to th
 185        }
 186
 19187        return string.Equals(recomputedHash.HashValue, signedGrant.CanonicalHash.HashValue, StringComparison.Ordinal)
 19188            ? null
 19189            : CapabilityGrantValidationResult.Failed(
 19190                grant,
 19191                CapabilityGrantValidationCategory.InvalidProof,
 19192                VerificationPolicyAction.Deny,
 19193                "capability.proof-content-mismatch",
 19194                "The grant does not hash to the signed canonical hash value.");
 0195    }
 196
 197#pragma warning disable IDE0046 // Preserve explicit ordered guard clauses for auditability.
 198    private static CapabilityGrantValidationResult? ValidateMetadata(
 199        CapabilityGrant grant,
 200        CapabilityGrantValidationOptions options,
 201        DateTimeOffset validationUtc)
 202    {
 53203        if (options.Issuer is not null
 53204            && !string.Equals(options.Issuer, grant.Issuer, StringComparison.Ordinal))
 205        {
 2206            return CapabilityGrantValidationResult.Failed(
 2207                grant,
 2208                CapabilityGrantValidationCategory.WrongIssuer,
 2209                VerificationPolicyAction.Deny,
 2210                "capability.issuer-mismatch");
 211        }
 212
 51213        if (options.Audience is not null
 51214            && !string.Equals(options.Audience, grant.Audience, StringComparison.Ordinal))
 215        {
 1216            return CapabilityGrantValidationResult.Failed(
 1217                grant,
 1218                CapabilityGrantValidationCategory.WrongAudience,
 1219                VerificationPolicyAction.Deny,
 1220                "capability.audience-mismatch");
 221        }
 222
 50223        if (options.ExpectedSubjectId is not null
 50224            && !string.Equals(options.ExpectedSubjectId, grant.SubjectId, StringComparison.Ordinal))
 225        {
 4226            return CapabilityGrantValidationResult.Failed(
 4227                grant,
 4228                CapabilityGrantValidationCategory.SubjectMismatch,
 4229                VerificationPolicyAction.Deny,
 4230                "capability.subject-mismatch");
 231        }
 232
 46233        if (options.ExpectedOperationName is not null
 46234            && !string.Equals(options.ExpectedOperationName, grant.OperationName, StringComparison.Ordinal))
 235        {
 3236            return CapabilityGrantValidationResult.Failed(
 3237                grant,
 3238                CapabilityGrantValidationCategory.OperationMismatch,
 3239                VerificationPolicyAction.Deny,
 3240                "capability.operation-mismatch");
 241        }
 242
 43243        if (IsNotYetValid(grant, validationUtc, options.AllowedClockSkew))
 244        {
 3245            return CapabilityGrantValidationResult.Failed(
 3246                grant,
 3247                CapabilityGrantValidationCategory.NotYetValid,
 3248                VerificationPolicyAction.Defer,
 3249                "capability.not-yet-valid");
 250        }
 251
 40252        if (IsExpired(grant, validationUtc, options.AllowedClockSkew))
 253        {
 4254            return CapabilityGrantValidationResult.Failed(
 4255                grant,
 4256                CapabilityGrantValidationCategory.Expired,
 4257                VerificationPolicyAction.Deny,
 4258                "capability.expired");
 259        }
 260
 36261        if (options.Scopes.Count > 0
 36262            && !ContainsRequiredScopes(grant.Scopes, options.Scopes))
 263        {
 3264            return CapabilityGrantValidationResult.Failed(
 3265                grant,
 3266                CapabilityGrantValidationCategory.WrongScope,
 3267                VerificationPolicyAction.Deny,
 3268                "capability.scope-missing");
 269        }
 270
 33271        if ((options.PolicyVersion is not null
 33272                && !string.Equals(options.PolicyVersion, grant.PolicyVersion, StringComparison.Ordinal))
 33273            || (options.PolicyHash is not null
 33274                && !string.Equals(options.PolicyHash, grant.PolicyHash, StringComparison.Ordinal)))
 275        {
 2276            return CapabilityGrantValidationResult.Failed(
 2277                grant,
 2278                CapabilityGrantValidationCategory.PolicyMismatch,
 2279                VerificationPolicyAction.Deny,
 2280                "capability.policy-mismatch");
 281        }
 282
 31283        if (options.RequireAcknowledgmentReference && !grant.HasAcknowledgmentReference)
 284        {
 2285            return CapabilityGrantValidationResult.Failed(
 2286                grant,
 2287                CapabilityGrantValidationCategory.MissingAcknowledgmentReference,
 2288                VerificationPolicyAction.RequireAcknowledgment,
 2289                "capability.acknowledgment-missing");
 290        }
 291
 29292        if (options.AcknowledgmentId is not null
 29293            && !string.Equals(options.AcknowledgmentId, grant.AcknowledgmentId, StringComparison.Ordinal))
 294        {
 1295            return CapabilityGrantValidationResult.Failed(
 1296                grant,
 1297                CapabilityGrantValidationCategory.AcknowledgmentMismatch,
 1298                VerificationPolicyAction.Deny,
 1299                "capability.acknowledgment-mismatch");
 300        }
 301
 28302        if (options.HandshakeId is not null
 28303            && !string.Equals(options.HandshakeId, grant.HandshakeId, StringComparison.Ordinal))
 304        {
 1305            return CapabilityGrantValidationResult.Failed(
 1306                grant,
 1307                CapabilityGrantValidationCategory.HandshakeMismatch,
 1308                VerificationPolicyAction.Deny,
 1309                "capability.handshake-mismatch");
 310        }
 311
 27312        if (options.GatewayBinding is not null
 27313            && !string.Equals(options.GatewayBinding, grant.GatewayBinding, StringComparison.Ordinal))
 314        {
 2315            return CapabilityGrantValidationResult.Failed(
 2316                grant,
 2317                CapabilityGrantValidationCategory.GatewayMismatch,
 2318                VerificationPolicyAction.Deny,
 2319                "capability.gateway-mismatch");
 320        }
 321
 25322        if (options.ResourceBinding is not null
 25323            && !string.Equals(options.ResourceBinding, grant.ResourceBinding, StringComparison.Ordinal))
 324        {
 1325            return CapabilityGrantValidationResult.Failed(
 1326                grant,
 1327                CapabilityGrantValidationCategory.ResourceMismatch,
 1328                VerificationPolicyAction.Deny,
 1329                "capability.resource-mismatch");
 330        }
 331
 24332        return null;
 333    }
 334#pragma warning restore IDE0046
 335
 336    private static bool IsNotYetValid(CapabilityGrant grant, DateTimeOffset validationUtc, TimeSpan allowedClockSkew)
 337    {
 43338        return grant.NotBeforeUtc.HasValue
 43339            && grant.NotBeforeUtc.Value > validationUtc
 43340            && grant.NotBeforeUtc.Value - validationUtc > allowedClockSkew;
 341    }
 342
 343    private static bool IsExpired(CapabilityGrant grant, DateTimeOffset validationUtc, TimeSpan allowedClockSkew)
 344    {
 40345        return validationUtc >= grant.ExpiresUtc
 40346            && validationUtc - grant.ExpiresUtc >= allowedClockSkew;
 347    }
 348
 349    private static async ValueTask<CapabilityGrantValidationResult?> ValidateUseAsync(
 350        CapabilityGrant grant,
 351        CapabilityGrantValidationOptions options,
 352        ICapabilityGrantUseStore? useStore,
 353        DateTimeOffset validationUtc,
 354        CancellationToken cancellationToken)
 355    {
 17356        if (useStore is null)
 357        {
 2358            return CapabilityGrantValidationResult.Failed(grant, CapabilityGrantValidationCategory.ReplayStoreUnavailabl
 359        }
 360
 361        // A limit the caller supplies is local policy; a limit the issuer bound into the signed payload is authority.
 362        // Taking the narrower of the two lets a relying party tighten the limit but never widen what was issued.
 15363        int effectiveMaxUseCount = grant.MaxUseCount.HasValue
 15364            ? Math.Min(grant.MaxUseCount.Value, options.MaxUseCount)
 15365            : options.MaxUseCount;
 366
 15367        CapabilityGrantUseResult result = await useStore
 15368            .TryConsumeAsync(grant, effectiveMaxUseCount, validationUtc, cancellationToken)
 15369            .ConfigureAwait(false);
 370
 15371        return result.State switch
 15372        {
 0373            GrantUseState.Unspecified => CapabilityGrantValidationResult.Failed(grant, CapabilityGrantValidationCategory
 7374            GrantUseState.Accepted => null,
 3375            GrantUseState.UseLimitExceeded => CapabilityGrantValidationResult.Failed(grant, CapabilityGrantValidationCat
 1376            GrantUseState.Stopped => CapabilityGrantValidationResult.Failed(grant, CapabilityGrantValidationCategory.Rev
 1377            GrantUseState.Cancelled => CapabilityGrantValidationResult.Failed(grant, CapabilityGrantValidationCategory.C
 2378            GrantUseState.Unavailable => CapabilityGrantValidationResult.Failed(grant, CapabilityGrantValidationCategory
 1379            _ => CapabilityGrantValidationResult.Failed(grant, CapabilityGrantValidationCategory.Failed, VerificationPol
 15380        };
 17381    }
 382
 383    private static bool ContainsRequiredScopes(IReadOnlyList<string> actualScopes, IReadOnlyList<string> requiredScopes)
 384    {
 30385        HashSet<string> actualScopeSet = new(actualScopes, StringComparer.Ordinal);
 386
 119387        foreach (string requiredScope in requiredScopes)
 388        {
 31389            if (!actualScopeSet.Contains(requiredScope))
 390            {
 3391                return false;
 392            }
 393        }
 394
 27395        return true;
 3396    }
 397
 398    private static CapabilityGrantValidationCategory MapVerificationCategory(SignatureVerificationCategory category)
 399    {
 13400        return category switch
 13401        {
 0402            SignatureVerificationCategory.Unspecified => CapabilityGrantValidationCategory.Failed,
 1403            SignatureVerificationCategory.MissingSignature => CapabilityGrantValidationCategory.MissingProof,
 0404            SignatureVerificationCategory.Valid => CapabilityGrantValidationCategory.Valid,
 1405            SignatureVerificationCategory.InvalidSignature => CapabilityGrantValidationCategory.InvalidProof,
 1406            SignatureVerificationCategory.HashMismatch => CapabilityGrantValidationCategory.InvalidProof,
 1407            SignatureVerificationCategory.RevokedKey => CapabilityGrantValidationCategory.Revoked,
 1408            SignatureVerificationCategory.UntrustedKey => CapabilityGrantValidationCategory.InvalidProof,
 3409            SignatureVerificationCategory.UntrustedSigningContext => CapabilityGrantValidationCategory.InvalidProof,
 1410            SignatureVerificationCategory.ProviderUnavailable => CapabilityGrantValidationCategory.Failed,
 1411            SignatureVerificationCategory.UnknownKeyVersion => CapabilityGrantValidationCategory.Failed,
 1412            SignatureVerificationCategory.CanonicalizationMismatch => CapabilityGrantValidationCategory.InvalidProof,
 1413            SignatureVerificationCategory.UnsupportedAlgorithm => CapabilityGrantValidationCategory.InvalidProof,
 1414            SignatureVerificationCategory.Failed => CapabilityGrantValidationCategory.Failed,
 0415            _ => CapabilityGrantValidationCategory.Failed
 13416        };
 417    }
 418}