< Summary

Information
Class: AsiBackbone.Core.Signing.SignedGovernanceArtifacts
Assembly: AsiBackbone.Core
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Signing/SignedGovernanceArtifact.cs
Line coverage
97%
Covered lines: 44
Uncovered lines: 1
Coverable lines: 45
Total lines: 227
Line coverage: 97.7%
Branch coverage
77%
Covered branches: 14
Total branches: 18
Branch coverage: 77.7%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
WithoutSignature(...)100%11100%
SigningReady(...)100%11100%
Rehydrate(...)100%22100%
FromSigningMetadata(...)100%11100%
MergeCanonicalHashMetadata(...)75%161695.24%

File(s)

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

#LineLine coverage
 1namespace AsiBackbone.Core.Signing;
 2
 3/// <summary>
 4/// Represents an AsiBackbone governance artifact together with its canonical payload, canonical hash,
 5/// and optional provider-neutral signing metadata.
 6/// </summary>
 7/// <typeparam name="TArtifact">The governance artifact type.</typeparam>
 8/// <remarks>
 9/// This type preserves the boundary between unsigned, signing-ready, and signed artifacts. A signed artifact
 10/// has provider metadata attached, but verification, immutable storage, hash chaining, and external anchoring
 11/// remain separate host or provider responsibilities.
 12/// </remarks>
 13public sealed class SignedGovernanceArtifact<TArtifact>
 14{
 15    internal SignedGovernanceArtifact(
 16        TArtifact artifact,
 17        CanonicalPayload canonicalPayload,
 18        CanonicalPayloadHash canonicalHash,
 19        SigningMetadata signingMetadata)
 20    {
 21        ArgumentNullException.ThrowIfNull(artifact);
 22        ArgumentNullException.ThrowIfNull(canonicalPayload);
 23        ArgumentNullException.ThrowIfNull(canonicalHash);
 24        ArgumentNullException.ThrowIfNull(signingMetadata);
 25
 26        if (!string.Equals(canonicalPayload.ArtifactType, canonicalHash.ArtifactType, StringComparison.Ordinal)
 27            || !string.Equals(canonicalPayload.ArtifactId, canonicalHash.ArtifactId, StringComparison.Ordinal)
 28            || !string.Equals(canonicalPayload.PayloadSchemaVersion, canonicalHash.PayloadSchemaVersion, StringCompariso
 29            || !string.Equals(canonicalPayload.CanonicalizationVersion, canonicalHash.CanonicalizationVersion, StringCom
 30        {
 31            throw new ArgumentException("The canonical payload and canonical hash must describe the same governance arti
 32        }
 33
 34        Artifact = artifact;
 35        CanonicalPayload = canonicalPayload;
 36        CanonicalHash = canonicalHash;
 37        SigningMetadata = signingMetadata;
 38    }
 39
 40    /// <summary>
 41    /// Gets the original governance artifact.
 42    /// </summary>
 43    public TArtifact Artifact { get; }
 44
 45    /// <summary>
 46    /// Gets the deterministic canonical payload used for hashing and signing.
 47    /// </summary>
 48    public CanonicalPayload CanonicalPayload { get; }
 49
 50    /// <summary>
 51    /// Gets the canonical payload hash metadata.
 52    /// </summary>
 53    public CanonicalPayloadHash CanonicalHash { get; }
 54
 55    /// <summary>
 56    /// Gets provider-neutral signing metadata. This may be empty, signing-ready, failed, or signed metadata.
 57    /// </summary>
 58    public SigningMetadata SigningMetadata { get; }
 59
 60    /// <summary>
 61    /// Gets the stable artifact type bound into the canonical payload and signing metadata.
 62    /// </summary>
 63    public string ArtifactType => CanonicalHash.ArtifactType;
 64
 65    /// <summary>
 66    /// Gets the stable artifact identifier bound into the canonical payload and signing metadata.
 67    /// </summary>
 68    public string ArtifactId => CanonicalHash.ArtifactId;
 69
 70    /// <summary>
 71    /// Gets the hash algorithm used to compute <see cref="SigningHash" />.
 72    /// </summary>
 73    public string HashAlgorithm => CanonicalHash.HashAlgorithm;
 74
 75    /// <summary>
 76    /// Gets the canonical payload hash value that is signed or made signing-ready.
 77    /// </summary>
 78    public string SigningHash => CanonicalHash.HashValue;
 79
 80    /// <summary>
 81    /// Gets a value indicating whether provider signing metadata includes a signature.
 82    /// </summary>
 83    public bool IsSigned => SigningMetadata.IsSigned;
 84
 85    /// <summary>
 86    /// Gets a value indicating whether hash metadata is present but no signature has been attached.
 87    /// </summary>
 88    public bool IsSigningReady => !SigningMetadata.HasSignature && SigningMetadata.SigningHash is not null;
 89
 90    /// <summary>
 91    /// Gets a value indicating whether the artifact carries no signing metadata.
 92    /// </summary>
 93    public bool HasNoSignature => !SigningMetadata.HasSignature && SigningMetadata.SigningHash is null;
 94}
 95
 96/// <summary>
 97/// Provides non-generic factories for creating signed governance artifact wrappers.
 98/// </summary>
 99public static class SignedGovernanceArtifacts
 100{
 101    /// <summary>
 102    /// Creates an artifact wrapper with no signing metadata attached.
 103    /// </summary>
 104    public static SignedGovernanceArtifact<TArtifact> WithoutSignature<TArtifact>(
 105        TArtifact artifact,
 106        CanonicalPayload canonicalPayload,
 107        CanonicalPayloadHash canonicalHash)
 108    {
 9109        return new SignedGovernanceArtifact<TArtifact>(
 9110            artifact,
 9111            canonicalPayload,
 9112            canonicalHash,
 9113            SigningMetadata.NoSignature);
 114    }
 115
 116    /// <summary>
 117    /// Creates a signing-ready artifact wrapper. Hash metadata is attached without implying that a signature exists.
 118    /// </summary>
 119    public static SignedGovernanceArtifact<TArtifact> SigningReady<TArtifact>(
 120        TArtifact artifact,
 121        CanonicalPayload canonicalPayload,
 122        CanonicalPayloadHash canonicalHash,
 123        IReadOnlyDictionary<string, string>? metadata = null)
 124    {
 6125        return new SignedGovernanceArtifact<TArtifact>(
 6126            artifact,
 6127            canonicalPayload,
 6128            canonicalHash,
 6129            canonicalHash.ToSigningMetadata(metadata));
 130    }
 131
 132    /// <summary>
 133    /// Creates an artifact wrapper from a stored canonical payload, hash, and signing metadata, verifying that the payl
 134    /// </summary>
 135    /// <remarks>
 136    /// Use this factory when rehydrating a previously signed artifact from storage, a queue, or any other channel outsi
 137    /// signing call itself. <see cref="FromSigningMetadata{TArtifact}" /> accepts the hash a caller supplies, which is 
 138    /// immediately after signing but does not establish that a stored payload still hashes to a stored hash. Rehydratio
 139    /// recomputes the hash and rejects the retained payload/hash pair when they disagree. It does not rebuild a payload
 140    /// <paramref name="artifact" /> and therefore does not authenticate that separately supplied typed object. Callers 
 141    /// consume the typed object should use <see cref="GovernanceArtifactVerifier.VerifyTypedAsync{TArtifact}" /> with t
 142    /// applicable canonical payload builder before trusting its values.
 143    /// </remarks>
 144    /// <exception cref="ArgumentException">The canonical payload does not hash to <paramref name="canonicalHash" />.</e
 145    /// <exception cref="NotSupportedException">The canonical hash algorithm is not supported by the built-in hasher.</e
 146    public static SignedGovernanceArtifact<TArtifact> Rehydrate<TArtifact>(
 147        TArtifact artifact,
 148        CanonicalPayload canonicalPayload,
 149        CanonicalPayloadHash canonicalHash,
 150        SigningMetadata signingMetadata)
 151    {
 3152        ArgumentNullException.ThrowIfNull(canonicalPayload);
 3153        ArgumentNullException.ThrowIfNull(canonicalHash);
 154
 3155        CanonicalPayloadHash recomputedHash = CanonicalPayloadHasher.ComputeHash(canonicalPayload, canonicalHash.HashAlg
 156
 3157        return string.Equals(recomputedHash.HashValue, canonicalHash.HashValue, StringComparison.Ordinal)
 3158            ? FromSigningMetadata(artifact, canonicalPayload, canonicalHash, signingMetadata)
 3159            : throw new ArgumentException(
 3160                "The canonical payload does not hash to the supplied canonical hash value.",
 3161                nameof(canonicalHash));
 162    }
 163
 164    /// <summary>
 165    /// Creates an artifact wrapper from signing metadata returned by a host or provider package.
 166    /// </summary>
 167    /// <remarks>
 168    /// The supplied <paramref name="canonicalHash" /> is trusted as given, which is appropriate immediately after a sig
 169    /// call produced it from <paramref name="canonicalPayload" />. Use <see cref="Rehydrate{TArtifact}" /> when the pay
 170    /// hash were read back from storage or received over a wire, so the pair is checked rather than assumed.
 171    /// </remarks>
 172    public static SignedGovernanceArtifact<TArtifact> FromSigningMetadata<TArtifact>(
 173        TArtifact artifact,
 174        CanonicalPayload canonicalPayload,
 175        CanonicalPayloadHash canonicalHash,
 176        SigningMetadata signingMetadata)
 177    {
 128178        ArgumentNullException.ThrowIfNull(signingMetadata);
 179
 128180        return new SignedGovernanceArtifact<TArtifact>(
 128181            artifact,
 128182            canonicalPayload,
 128183            canonicalHash,
 128184            MergeCanonicalHashMetadata(canonicalHash, signingMetadata));
 185    }
 186
 187    private static SigningMetadata MergeCanonicalHashMetadata(
 188        CanonicalPayloadHash canonicalHash,
 189        SigningMetadata signingMetadata)
 190    {
 128191        if (signingMetadata.SigningHash is not null
 128192            && !string.Equals(signingMetadata.SigningHash, canonicalHash.HashValue, StringComparison.Ordinal))
 193        {
 0194            throw new ArgumentException("Signing metadata hash must match the canonical payload hash.", nameof(signingMe
 195        }
 196
 128197        Dictionary<string, string> metadata = new(StringComparer.Ordinal);
 198
 1280199        foreach (KeyValuePair<string, string> item in canonicalHash.ToSigningMetadata().Metadata)
 200        {
 512201            metadata[item.Key] = item.Value;
 202        }
 203
 794204        foreach (KeyValuePair<string, string> item in signingMetadata.Metadata)
 205        {
 269206            if (string.IsNullOrWhiteSpace(item.Key))
 207            {
 208                continue;
 209            }
 210
 269211            metadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty;
 212        }
 213
 128214        return SigningMetadata.Create(
 128215            signingHash: canonicalHash.HashValue,
 128216            hashAlgorithm: string.IsNullOrWhiteSpace(signingMetadata.HashAlgorithm)
 128217                ? canonicalHash.HashAlgorithm
 128218                : signingMetadata.HashAlgorithm,
 128219            signature: signingMetadata.Signature,
 128220            signatureAlgorithm: signingMetadata.SignatureAlgorithm,
 128221            keyId: signingMetadata.KeyId,
 128222            keyVersion: signingMetadata.KeyVersion,
 128223            provider: signingMetadata.Provider,
 128224            signedUtc: signingMetadata.SignedUtc,
 128225            metadata: metadata);
 226    }
 227}