< Summary

Information
Class: AsiBackbone.Core.Integrity.AuditIntegrityVerifier
Assembly: AsiBackbone.Core
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Integrity/AuditIntegrityVerifier.cs
Line coverage
100%
Covered lines: 93
Uncovered lines: 0
Coverable lines: 93
Total lines: 153
Line coverage: 100%
Branch coverage
96%
Covered branches: 31
Total branches: 32
Branch coverage: 96.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Verify(...)90%1010100%
VerifyLink(...)100%2222100%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Integrity/AuditIntegrityVerifier.cs

#LineLine coverage
 1using AsiBackbone.Core.Signing;
 2
 3namespace AsiBackbone.Core.Integrity;
 4
 5/// <summary>
 6/// Verifies provider-neutral append-only audit integrity chains.
 7/// </summary>
 8public static class AuditIntegrityVerifier
 9{
 10    /// <summary>
 11    /// Verifies that the supplied links form one continuous append-only chain in the supplied order.
 12    /// </summary>
 13    public static AuditIntegrityVerificationResult Verify(
 14        IEnumerable<AuditIntegrityLink> links,
 15        string? expectedChainId = null,
 16        bool requireGenesis = true)
 17    {
 3218        ArgumentNullException.ThrowIfNull(links);
 19
 3220        List<AuditIntegrityLink> orderedLinks = [.. links];
 21
 3222        if (orderedLinks.Count == 0)
 23        {
 224            return AuditIntegrityVerificationResult.Failed(
 225                AuditIntegrityVerificationCategory.EmptyChain,
 226                "integrity.chain-empty",
 227                "No integrity links were supplied.");
 28        }
 29
 3030        string chainId = string.IsNullOrWhiteSpace(expectedChainId)
 3031            ? orderedLinks[0].ChainId
 3032            : expectedChainId.Trim();
 3033        HashSet<long> observedSequences = [];
 3034        string expectedPreviousHash = string.Empty;
 3035        long expectedSequence = requireGenesis ? 1 : orderedLinks[0].Sequence;
 36
 15837        foreach (AuditIntegrityLink link in orderedLinks)
 38        {
 6239            AuditIntegrityVerificationResult? result = VerifyLink(
 6240                link,
 6241                chainId,
 6242                expectedSequence,
 6243                expectedPreviousHash,
 6244                observedSequences,
 6245                requireGenesis);
 46
 6247            if (result is not null)
 48            {
 2649                return result;
 50            }
 51
 3652            _ = observedSequences.Add(link.Sequence);
 3653            expectedPreviousHash = link.LinkHash;
 3654            expectedSequence = link.Sequence + 1;
 55        }
 56
 457        AuditIntegrityLink tip = orderedLinks[^1];
 458        return AuditIntegrityVerificationResult.Valid(chainId, orderedLinks.Count, tip.LinkHash);
 2659    }
 60
 61    private static AuditIntegrityVerificationResult? VerifyLink(
 62        AuditIntegrityLink link,
 63        string expectedChainId,
 64        long expectedSequence,
 65        string expectedPreviousHash,
 66        HashSet<long> observedSequences,
 67        bool requireGenesis)
 68    {
 6269        if (!string.Equals(link.HashAlgorithm, CanonicalPayloadOptions.DefaultHashAlgorithm, StringComparison.Ordinal))
 70        {
 271            return AuditIntegrityVerificationResult.Failed(
 272                AuditIntegrityVerificationCategory.UnsupportedAlgorithm,
 273                "integrity.hash-algorithm-unsupported",
 274                "The integrity link uses an unsupported hash algorithm.",
 275                link);
 76        }
 77
 6078        if (!string.Equals(link.ChainId, expectedChainId, StringComparison.Ordinal))
 79        {
 280            return AuditIntegrityVerificationResult.Failed(
 281                AuditIntegrityVerificationCategory.WrongChain,
 282                "integrity.chain-id-mismatch",
 283                "The integrity link belongs to a different chain.",
 284                link);
 85        }
 86
 5887        if (observedSequences.Contains(link.Sequence))
 88        {
 889            return AuditIntegrityVerificationResult.Failed(
 890                AuditIntegrityVerificationCategory.ForkedChain,
 891                "integrity.sequence-duplicate",
 892                "Multiple links claim the same chain sequence.",
 893                link);
 94        }
 95
 5096        if (link.Sequence != expectedSequence)
 97        {
 698            AuditIntegrityVerificationCategory category = link.Sequence > expectedSequence
 699                ? AuditIntegrityVerificationCategory.MissingRecord
 6100                : AuditIntegrityVerificationCategory.ReorderedRecord;
 101
 6102            return AuditIntegrityVerificationResult.Failed(
 6103                category,
 6104                category is AuditIntegrityVerificationCategory.MissingRecord
 6105                    ? "integrity.sequence-missing"
 6106                    : "integrity.sequence-reordered",
 6107                "The integrity link sequence is not continuous in the supplied order.",
 6108                link,
 6109                new Dictionary<string, string>(StringComparer.Ordinal)
 6110                {
 6111                    ["expected_sequence"] = expectedSequence.ToString(System.Globalization.CultureInfo.InvariantCulture)
 6112                    ["actual_sequence"] = link.Sequence.ToString(System.Globalization.CultureInfo.InvariantCulture)
 6113                });
 114        }
 115
 44116        if (requireGenesis && link.Sequence == 1 && link.PreviousLinkHash.Length != 0)
 117        {
 2118            return AuditIntegrityVerificationResult.Failed(
 2119                AuditIntegrityVerificationCategory.HashMismatch,
 2120                "integrity.genesis-previous-hash-present",
 2121                "The genesis link must not point to a previous link hash.",
 2122                link);
 123        }
 124
 42125        if (!string.Equals(link.PreviousLinkHash, expectedPreviousHash, StringComparison.Ordinal))
 126        {
 4127            return AuditIntegrityVerificationResult.Failed(
 4128                AuditIntegrityVerificationCategory.HashMismatch,
 4129                "integrity.previous-link-hash-mismatch",
 4130                "The integrity link does not point to the previous link hash.",
 4131                link,
 4132                new Dictionary<string, string>(StringComparer.Ordinal)
 4133                {
 4134                    ["expected_previous_hash"] = expectedPreviousHash,
 4135                    ["actual_previous_hash"] = link.PreviousLinkHash
 4136                });
 137        }
 138
 38139        string expectedLinkHash = link.ComputeExpectedLinkHash();
 38140        return !string.Equals(link.LinkHash, expectedLinkHash, StringComparison.Ordinal)
 38141            ? AuditIntegrityVerificationResult.Failed(
 38142                AuditIntegrityVerificationCategory.ModifiedRecord,
 38143                "integrity.link-hash-mismatch",
 38144                "The integrity link hash no longer matches its canonical fields.",
 38145                link,
 38146                new Dictionary<string, string>(StringComparer.Ordinal)
 38147                {
 38148                    ["expected_link_hash"] = expectedLinkHash,
 38149                    ["actual_link_hash"] = link.LinkHash
 38150                })
 38151            : null;
 152    }
 153}