< Summary

Information
Class: AsiBackbone.Core.Signing.GovernanceArtifactVerifier
Assembly: AsiBackbone.Core
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Signing/GovernanceArtifactVerifier.cs
Line coverage
96%
Covered lines: 169
Uncovered lines: 6
Coverable lines: 175
Total lines: 351
Line coverage: 96.5%
Branch coverage
94%
Covered branches: 72
Total branches: 76
Branch coverage: 94.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Signing/GovernanceArtifactVerifier.cs

#LineLine coverage
 1namespace AsiBackbone.Core.Signing;
 2
 3/// <summary>
 4/// Provides provider-neutral helpers for verifying signed governance artifacts and applying verification policy.
 5/// </summary>
 6/// <remarks>
 7/// The verifier wrapper does not resolve provider-specific keys in Core and does not imply legal evidence, compliance c
 8/// </remarks>
 9public static class GovernanceArtifactVerifier
 10{
 11    /// <summary>
 12    /// Verifies a signed governance artifact's retained canonical payload and maps the result to a host-facing policy o
 13    /// </summary>
 14    /// <remarks>
 15    /// This payload-only path establishes that <see cref="SignedGovernanceArtifact{TArtifact}.CanonicalPayload" /> hash
 16    /// the signed canonical hash. It does not establish that <see cref="SignedGovernanceArtifact{TArtifact}.Artifact" /
 17    /// corresponds to that payload. Use <see cref="VerifyTypedAsync{TArtifact}" /> when the typed artifact will be cons
 18    /// after verification.
 19    /// </remarks>
 20    public static async ValueTask<VerificationPolicyOutcome> VerifyAsync<TArtifact>(
 21        SignedGovernanceArtifact<TArtifact> artifact,
 22        IGovernanceSignatureVerificationService verificationService,
 23        VerificationPolicyOptions? options = null,
 24        VerificationPolicyContext? context = null,
 25        CancellationToken cancellationToken = default)
 26    {
 5327        return await VerifyCoreAsync(
 5328            artifact,
 5329            verificationService,
 5330            canonicalPayloadBuilder: null,
 5331            options,
 5332            context,
 5333            cancellationToken).ConfigureAwait(false);
 5334    }
 35
 36    /// <summary>
 37    /// Verifies a signed governance artifact, binds its typed artifact to the signed canonical payload, and maps the re
 38    /// to a host-facing policy outcome.
 39    /// </summary>
 40    /// <remarks>
 41    /// The supplied builder must use the same canonicalization options and schema rules that were used when the artifac
 42    /// was signed. The rebuilt payload is hashed with the recorded hash algorithm and compared with the signed canonica
 43    /// hash before the verification provider is called. A mismatch fails closed with
 44    /// <c>signature.typed-artifact-mismatch</c> in the <see cref="SignatureVerificationCategory.HashMismatch" /> catego
 45    /// </remarks>
 46    public static async ValueTask<VerificationPolicyOutcome> VerifyTypedAsync<TArtifact>(
 47        SignedGovernanceArtifact<TArtifact> artifact,
 48        IGovernanceSignatureVerificationService verificationService,
 49        Func<TArtifact, CanonicalPayload> canonicalPayloadBuilder,
 50        VerificationPolicyOptions? options = null,
 51        VerificationPolicyContext? context = null,
 52        CancellationToken cancellationToken = default)
 53    {
 954        ArgumentNullException.ThrowIfNull(canonicalPayloadBuilder);
 55
 956        return await VerifyCoreAsync(
 957            artifact,
 958            verificationService,
 959            canonicalPayloadBuilder,
 960            options,
 961            context,
 962            cancellationToken).ConfigureAwait(false);
 963    }
 64
 65    private static async ValueTask<VerificationPolicyOutcome> VerifyCoreAsync<TArtifact>(
 66        SignedGovernanceArtifact<TArtifact> artifact,
 67        IGovernanceSignatureVerificationService verificationService,
 68        Func<TArtifact, CanonicalPayload>? canonicalPayloadBuilder,
 69        VerificationPolicyOptions? options,
 70        VerificationPolicyContext? context,
 71        CancellationToken cancellationToken)
 72    {
 6273        ArgumentNullException.ThrowIfNull(artifact);
 6274        ArgumentNullException.ThrowIfNull(verificationService);
 6275        cancellationToken.ThrowIfCancellationRequested();
 76
 6277        VerificationPolicyContext effectiveContext = context ?? VerificationPolicyContext.Default;
 6278        SignatureVerificationResult? preflightResult = ValidateBeforeProvider(
 6279            artifact,
 6280            effectiveContext,
 6281            canonicalPayloadBuilder);
 82
 6283        if (preflightResult is not null)
 84        {
 2285            return VerificationPolicyEvaluator.Evaluate(artifact, preflightResult, options);
 86        }
 87
 88        try
 89        {
 90            // The version 1 input binds the canonical descriptors, hash, and signing policy context, so a relabeled
 91            // policy_version or policy_hash no longer verifies. Previously the provider was asked to verify the hash te
 92            // alone and every signing metadata label was unauthenticated.
 4093            SignatureVerificationResult verificationResult = await verificationService
 4094                .VerifyAsync(
 4095                    CreateVerificationRequest(
 4096                        artifact,
 4097                        effectiveContext,
 4098                        GovernanceSignatureInput.CreateV1(artifact.CanonicalHash, artifact.SigningMetadata.Metadata)),
 4099                    cancellationToken)
 40100                .ConfigureAwait(false);
 101
 36102            if (!verificationResult.IsValid
 36103                && effectiveContext.AllowLegacySignatureInput
 36104                && VerificationPolicyEvaluator.Categorize(verificationResult) is SignatureVerificationCategory.InvalidSi
 105            {
 2106                verificationResult = await VerifyLegacySignatureInputAsync(
 2107                    artifact,
 2108                    verificationService,
 2109                    effectiveContext,
 2110                    verificationResult,
 2111                    cancellationToken).ConfigureAwait(false);
 112            }
 113
 36114            return VerificationPolicyEvaluator.Evaluate(artifact, verificationResult, options);
 115        }
 2116        catch (InvalidOperationException exception)
 117        {
 2118            return CreateProviderUnavailableOutcome(artifact, options, exception);
 119        }
 1120        catch (NotSupportedException exception)
 121        {
 1122            return CreateProviderUnavailableOutcome(artifact, options, exception);
 123        }
 1124        catch (TimeoutException exception)
 125        {
 1126            return CreateProviderUnavailableOutcome(artifact, options, exception);
 127        }
 62128    }
 129
 130    /// <summary>
 131    /// Verifies a pre-6.0 artifact against the hash-only signature input after version 1 verification failed.
 132    /// </summary>
 133    /// <remarks>
 134    /// Runs only when the host opted in through <see cref="VerificationPolicyContext.WithLegacySignatureInputAllowed" /
 135    /// and the version 1 attempt failed as an invalid signature. A legacy signature authenticates the canonical payload
 136    /// only, so the policy labels it carries are unauthenticated and cannot satisfy a policy pin. If the legacy attempt
 137    /// also fails, the version 1 failure is reported, because it describes the current format.
 138    /// </remarks>
 139    private static async ValueTask<SignatureVerificationResult> VerifyLegacySignatureInputAsync<TArtifact>(
 140        SignedGovernanceArtifact<TArtifact> artifact,
 141        IGovernanceSignatureVerificationService verificationService,
 142        VerificationPolicyContext context,
 143        SignatureVerificationResult versionOneResult,
 144        CancellationToken cancellationToken)
 145    {
 146#pragma warning disable ASIB902 // Explicit legacy-verification path requested by the host.
 2147        SignatureVerificationResult legacyResult = await verificationService
 2148            .VerifyAsync(
 2149                CreateVerificationRequest(
 2150                    artifact,
 2151                    context,
 2152                    GovernanceSignatureInput.CreateLegacy(artifact.SigningHash)),
 2153                cancellationToken)
 2154            .ConfigureAwait(false);
 155#pragma warning restore ASIB902
 156
 2157        return !legacyResult.IsValid
 2158            ? versionOneResult
 2159            : context.ExpectedPolicyVersion is not null || context.ExpectedPolicyHash is not null
 2160            ? SignatureVerificationResult.Failed(
 2161                "signature.policy-context-not-authenticated",
 2162                SignatureVerificationCategory.UntrustedSigningContext,
 2163                "The artifact carries a pre-6.0 signature that does not cover the signing policy context, so it cannot s
 2164            : legacyResult;
 2165    }
 166
 167    private static SignatureVerificationRequest CreateVerificationRequest<TArtifact>(
 168        SignedGovernanceArtifact<TArtifact> artifact,
 169        VerificationPolicyContext context,
 170        ReadOnlyMemory<byte> signatureInput)
 171    {
 42172        return new SignatureVerificationRequest(
 42173            artifact.SigningHash,
 42174            artifact.SigningMetadata,
 42175            purpose: context.Purpose ?? artifact.ArtifactType,
 42176            metadata: context.Metadata)
 42177        {
 42178            SignatureInput = signatureInput
 42179        };
 180    }
 181
 182    private static VerificationPolicyOutcome CreateProviderUnavailableOutcome<TArtifact>(
 183        SignedGovernanceArtifact<TArtifact> artifact,
 184        VerificationPolicyOptions? options,
 185        Exception exception)
 186    {
 4187        var providerUnavailableResult = SignatureVerificationResult.Failed(
 4188            "signature.provider-unavailable",
 4189            SignatureVerificationCategory.ProviderUnavailable,
 4190            exception.GetType().Name);
 191
 4192        return VerificationPolicyEvaluator.Evaluate(artifact, providerUnavailableResult, options);
 193    }
 194
 195    private static SignatureVerificationResult? ValidateBeforeProvider<TArtifact>(
 196        SignedGovernanceArtifact<TArtifact> artifact,
 197        VerificationPolicyContext context,
 198        Func<TArtifact, CanonicalPayload>? canonicalPayloadBuilder)
 199    {
 62200        SignatureVerificationResult? metadataResult = ValidateSigningMetadata(artifact, context);
 201
 62202        if (metadataResult is not null)
 203        {
 20204            return metadataResult;
 205        }
 206
 42207        SignatureVerificationResult? canonicalBindingResult = ValidateCanonicalBinding(artifact);
 208
 42209        return canonicalBindingResult ?? ValidateTypedArtifactBinding(artifact, canonicalPayloadBuilder);
 210    }
 211
 212    private static SignatureVerificationResult? ValidateTypedArtifactBinding<TArtifact>(
 213        SignedGovernanceArtifact<TArtifact> artifact,
 214        Func<TArtifact, CanonicalPayload>? canonicalPayloadBuilder)
 215    {
 41216        if (canonicalPayloadBuilder is null)
 217        {
 32218            return null;
 219        }
 220
 9221        CanonicalPayload rebuiltPayload = canonicalPayloadBuilder(artifact.Artifact)
 9222            ?? throw new InvalidOperationException("The canonical payload builder returned null.");
 9223        CanonicalPayloadHash rebuiltHash = CanonicalPayloadHasher.ComputeHash(rebuiltPayload, artifact.HashAlgorithm);
 224
 9225        return string.Equals(rebuiltHash.HashValue, artifact.CanonicalHash.HashValue, StringComparison.Ordinal)
 9226            ? null
 9227            : SignatureVerificationResult.Failed(
 9228                "signature.typed-artifact-mismatch",
 9229                SignatureVerificationCategory.HashMismatch,
 9230                "The typed artifact does not rebuild to the signed canonical payload hash.");
 231    }
 232
 233    /// <summary>
 234    /// Recomputes the canonical payload hash and rejects an artifact whose signed hash is not the hash of its own canon
 235    /// </summary>
 236    /// <remarks>
 237    /// Without this step the provider verifies a signature over a hash the artifact carries about itself, which leaves 
 238    /// artifact content unbound to the signature. A caller that rehydrates an artifact from storage or a queue can othe
 239    /// present modified content beside an authentic hash and signature pair and receive a valid outcome.
 240    /// </remarks>
 241    private static SignatureVerificationResult? ValidateCanonicalBinding<TArtifact>(
 242        SignedGovernanceArtifact<TArtifact> artifact)
 243    {
 244        CanonicalPayloadHash recomputedHash;
 245
 246        try
 247        {
 42248            recomputedHash = CanonicalPayloadHasher.ComputeHash(artifact.CanonicalPayload, artifact.HashAlgorithm);
 42249        }
 0250        catch (NotSupportedException)
 251        {
 0252            return SignatureVerificationResult.Failed(
 0253                "signature.hash-algorithm-unsupported",
 0254                SignatureVerificationCategory.UnsupportedAlgorithm,
 0255                "The canonical payload hash cannot be recomputed with the built-in hasher, so the signed hash is not bou
 256        }
 257
 42258        return string.Equals(recomputedHash.HashValue, artifact.CanonicalHash.HashValue, StringComparison.Ordinal)
 42259            ? null
 42260            : SignatureVerificationResult.Failed(
 42261                "signature.hash-mismatch",
 42262                SignatureVerificationCategory.HashMismatch,
 42263                "The canonical payload does not hash to the signed canonical hash value.");
 0264    }
 265
 266    private static SignatureVerificationResult? ValidateSigningMetadata<TArtifact>(
 267        SignedGovernanceArtifact<TArtifact> artifact,
 268        VerificationPolicyContext context)
 269    {
 62270        SigningMetadata metadata = artifact.SigningMetadata;
 271
 62272        return artifact.HasNoSignature || !metadata.HasSignature
 62273            ? SignatureVerificationResult.MissingSignature("The governance artifact does not carry signature metadata.")
 62274            : string.IsNullOrWhiteSpace(metadata.SigningHash)
 62275            ? SignatureVerificationResult.MissingSignature("The governance artifact does not carry the hash that was sig
 62276            : !string.Equals(metadata.SigningHash, artifact.SigningHash, StringComparison.Ordinal)
 62277            ? SignatureVerificationResult.Failed(
 62278                "signature.hash-mismatch",
 62279                SignatureVerificationCategory.HashMismatch,
 62280                "The signing metadata hash does not match the canonical artifact hash.")
 62281            : metadata.HashAlgorithm is not null
 62282            && !string.Equals(metadata.HashAlgorithm, artifact.HashAlgorithm, StringComparison.OrdinalIgnoreCase)
 62283            ? SignatureVerificationResult.Failed(
 62284                "signature.hash-algorithm-unsupported",
 62285                SignatureVerificationCategory.HashMismatch,
 62286                "The signing metadata hash algorithm does not match the canonical artifact hash algorithm.")
 62287            : context.RequiredHashAlgorithm is not null
 62288            && !string.Equals(context.RequiredHashAlgorithm, artifact.HashAlgorithm, StringComparison.OrdinalIgnoreCase)
 62289            ? SignatureVerificationResult.Failed(
 62290                "signature.hash-algorithm-unsupported",
 62291                SignatureVerificationCategory.HashMismatch,
 62292                "The canonical artifact hash algorithm does not match the required verification policy algorithm.")
 62293            : !MatchesCanonicalMetadata(metadata, "artifact_id", artifact.ArtifactId)
 62294            || !MatchesCanonicalMetadata(metadata, "artifact_type", artifact.ArtifactType)
 62295            || !MatchesCanonicalMetadata(metadata, "canonicalization_version", artifact.CanonicalHash.CanonicalizationVe
 62296            || !MatchesCanonicalMetadata(metadata, "payload_schema_version", artifact.CanonicalHash.PayloadSchemaVersion
 62297            ? SignatureVerificationResult.Failed(
 62298                "signature.canonicalization-mismatch",
 62299                SignatureVerificationCategory.CanonicalizationMismatch,
 62300                "The signing metadata canonical artifact descriptors do not match the artifact being verified.")
 62301            : context.ExpectedKeyId is not null
 62302            && !string.Equals(context.ExpectedKeyId, metadata.KeyId, StringComparison.Ordinal)
 62303            ? SignatureVerificationResult.Failed(
 62304                "signature.key-not-trusted",
 62305                SignatureVerificationCategory.UntrustedKey,
 62306                "The signing key identifier does not match the verification policy expectation.")
 62307            : context.ExpectedKeyVersion is not null
 62308            && !string.Equals(context.ExpectedKeyVersion, metadata.KeyVersion, StringComparison.Ordinal)
 62309            ? SignatureVerificationResult.Failed(
 62310                "signature.key-not-trusted",
 62311                SignatureVerificationCategory.UntrustedKey,
 62312                "The signing key version does not match the verification policy expectation.")
 62313            // A provider or policy-context pin mismatch is a trust decision, not an outage. Reporting it as
 62314            // ProviderUnavailable (Defer) or CanonicalizationMismatch (formerly Escalate) gave an artifact signed under
 62315            // wrong provider or policy a softer outcome than a bad signature, the same defect 5.0 corrected for key pin
 62316            : context.RequiredProvider is not null
 62317            && !string.Equals(context.RequiredProvider, metadata.Provider, StringComparison.Ordinal)
 62318            ? SignatureVerificationResult.Failed(
 62319                "signature.provider-not-trusted",
 62320                SignatureVerificationCategory.UntrustedSigningContext,
 62321                "The signing provider does not match the required verification policy provider.")
 62322            : !MatchesOptionalPolicyMetadata(metadata, "policy_version", context.ExpectedPolicyVersion)
 62323            || !MatchesOptionalPolicyMetadata(metadata, "policy_hash", context.ExpectedPolicyHash)
 62324            ? SignatureVerificationResult.Failed(
 62325                "signature.policy-context-not-trusted",
 62326                SignatureVerificationCategory.UntrustedSigningContext,
 62327                "The signing metadata policy context does not match the verification policy expectation.")
 62328            : null;
 329    }
 330
 331    /// <summary>
 332    /// Requires a canonical descriptor to be present in signing metadata and to match the artifact being verified.
 333    /// </summary>
 334    /// <remarks>
 335    /// An absent descriptor previously matched anything, which let a signature produced for one artifact type or identi
 336    /// be presented alongside a different artifact. The shipped factories always write these descriptors, so a signed
 337    /// artifact that is missing one did not come from a canonical signing path.
 338    /// </remarks>
 339    private static bool MatchesCanonicalMetadata(SigningMetadata metadata, string key, string expectedValue)
 340    {
 216341        return metadata.Metadata.TryGetValue(key, out string? value)
 216342            && string.Equals(value, expectedValue, StringComparison.Ordinal);
 343    }
 344
 345    private static bool MatchesOptionalPolicyMetadata(SigningMetadata metadata, string key, string? expectedValue)
 346    {
 90347        return expectedValue is null
 90348            || (metadata.Metadata.TryGetValue(key, out string? value)
 90349                && string.Equals(value, expectedValue, StringComparison.Ordinal));
 350    }
 351}