< 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: 94
Uncovered lines: 0
Coverable lines: 94
Total lines: 155
Line coverage: 100%
Branch coverage
90%
Covered branches: 29
Total branches: 32
Branch coverage: 90.6%
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(...)90.9%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    {
 1018        ArgumentNullException.ThrowIfNull(links);
 19
 1020        List<AuditIntegrityLink> orderedLinks = [.. links];
 21
 1022        if (orderedLinks.Count == 0)
 23        {
 124            return AuditIntegrityVerificationResult.Failed(
 125                AuditIntegrityVerificationCategory.EmptyChain,
 126                "integrity.chain-empty",
 127                "No integrity links were supplied.");
 28        }
 29
 930        string chainId = string.IsNullOrWhiteSpace(expectedChainId)
 931            ? orderedLinks[0].ChainId
 932            : expectedChainId.Trim();
 933        HashSet<long> observedSequences = [];
 934        string expectedPreviousHash = string.Empty;
 935        long expectedSequence = requireGenesis ? 1 : orderedLinks[0].Sequence;
 36
 4137        foreach (AuditIntegrityLink link in orderedLinks)
 38        {
 1539            AuditIntegrityVerificationResult? result = VerifyLink(
 1540                link,
 1541                chainId,
 1542                expectedSequence,
 1543                expectedPreviousHash,
 1544                observedSequences,
 1545                requireGenesis);
 46
 1547            if (result is not null)
 48            {
 749                return result;
 50            }
 51
 852            _ = observedSequences.Add(link.Sequence);
 853            expectedPreviousHash = link.LinkHash;
 854            expectedSequence = link.Sequence + 1;
 55        }
 56
 257        AuditIntegrityLink tip = orderedLinks[^1];
 258        return AuditIntegrityVerificationResult.Valid(chainId, orderedLinks.Count, tip.LinkHash);
 759    }
 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    {
 1569        if (!string.Equals(link.HashAlgorithm, CanonicalPayloadOptions.DefaultHashAlgorithm, StringComparison.Ordinal))
 70        {
 171            return AuditIntegrityVerificationResult.Failed(
 172                AuditIntegrityVerificationCategory.UnsupportedAlgorithm,
 173                "integrity.hash-algorithm-unsupported",
 174                "The integrity link uses an unsupported hash algorithm.",
 175                link);
 76        }
 77
 1478        if (!string.Equals(link.ChainId, expectedChainId, StringComparison.Ordinal))
 79        {
 180            return AuditIntegrityVerificationResult.Failed(
 181                AuditIntegrityVerificationCategory.WrongChain,
 182                "integrity.chain-id-mismatch",
 183                "The integrity link belongs to a different chain.",
 184                link);
 85        }
 86
 1387        if (!observedSequences.Add(link.Sequence))
 88        {
 189            return AuditIntegrityVerificationResult.Failed(
 190                AuditIntegrityVerificationCategory.ForkedChain,
 191                "integrity.sequence-duplicate",
 192                "Multiple links claim the same chain sequence.",
 193                link);
 94        }
 95
 1296        _ = observedSequences.Remove(link.Sequence);
 97
 1298        if (link.Sequence != expectedSequence)
 99        {
 1100            AuditIntegrityVerificationCategory category = link.Sequence > expectedSequence
 1101                ? AuditIntegrityVerificationCategory.MissingRecord
 1102                : AuditIntegrityVerificationCategory.ReorderedRecord;
 103
 1104            return AuditIntegrityVerificationResult.Failed(
 1105                category,
 1106                category is AuditIntegrityVerificationCategory.MissingRecord
 1107                    ? "integrity.sequence-missing"
 1108                    : "integrity.sequence-reordered",
 1109                "The integrity link sequence is not continuous in the supplied order.",
 1110                link,
 1111                new Dictionary<string, string>(StringComparer.Ordinal)
 1112                {
 1113                    ["expected_sequence"] = expectedSequence.ToString(System.Globalization.CultureInfo.InvariantCulture)
 1114                    ["actual_sequence"] = link.Sequence.ToString(System.Globalization.CultureInfo.InvariantCulture)
 1115                });
 116        }
 117
 11118        if (requireGenesis && link.Sequence == 1 && link.PreviousLinkHash.Length != 0)
 119        {
 1120            return AuditIntegrityVerificationResult.Failed(
 1121                AuditIntegrityVerificationCategory.HashMismatch,
 1122                "integrity.genesis-previous-hash-present",
 1123                "The genesis link must not point to a previous link hash.",
 1124                link);
 125        }
 126
 10127        if (!string.Equals(link.PreviousLinkHash, expectedPreviousHash, StringComparison.Ordinal))
 128        {
 1129            return AuditIntegrityVerificationResult.Failed(
 1130                AuditIntegrityVerificationCategory.HashMismatch,
 1131                "integrity.previous-link-hash-mismatch",
 1132                "The integrity link does not point to the previous link hash.",
 1133                link,
 1134                new Dictionary<string, string>(StringComparer.Ordinal)
 1135                {
 1136                    ["expected_previous_hash"] = expectedPreviousHash,
 1137                    ["actual_previous_hash"] = link.PreviousLinkHash
 1138                });
 139        }
 140
 9141        string expectedLinkHash = link.ComputeExpectedLinkHash();
 9142        return !string.Equals(link.LinkHash, expectedLinkHash, StringComparison.Ordinal)
 9143            ? AuditIntegrityVerificationResult.Failed(
 9144                AuditIntegrityVerificationCategory.ModifiedRecord,
 9145                "integrity.link-hash-mismatch",
 9146                "The integrity link hash no longer matches its canonical fields.",
 9147                link,
 9148                new Dictionary<string, string>(StringComparer.Ordinal)
 9149                {
 9150                    ["expected_link_hash"] = expectedLinkHash,
 9151                    ["actual_link_hash"] = link.LinkHash
 9152                })
 9153            : null;
 154    }
 155}