< Summary

Information
Class: AsiBackbone.Core.Signing.SignedGovernanceArtifact<T>
Assembly: AsiBackbone.Core
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Signing/SignedGovernanceArtifact.cs
Line coverage
100%
Covered lines: 26
Uncovered lines: 0
Coverable lines: 26
Total lines: 227
Line coverage: 100%
Branch coverage
83%
Covered branches: 10
Total branches: 12
Branch coverage: 83.3%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)75%88100%
get_ArtifactType()100%11100%
get_ArtifactId()100%11100%
get_HashAlgorithm()100%11100%
get_SigningHash()100%11100%
get_IsSigned()100%11100%
get_IsSigningReady()100%22100%
get_HasNoSignature()100%22100%

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{
 14615    internal SignedGovernanceArtifact(
 14616        TArtifact artifact,
 14617        CanonicalPayload canonicalPayload,
 14618        CanonicalPayloadHash canonicalHash,
 14619        SigningMetadata signingMetadata)
 20    {
 14621        ArgumentNullException.ThrowIfNull(artifact);
 14622        ArgumentNullException.ThrowIfNull(canonicalPayload);
 14623        ArgumentNullException.ThrowIfNull(canonicalHash);
 14624        ArgumentNullException.ThrowIfNull(signingMetadata);
 25
 14626        if (!string.Equals(canonicalPayload.ArtifactType, canonicalHash.ArtifactType, StringComparison.Ordinal)
 14627            || !string.Equals(canonicalPayload.ArtifactId, canonicalHash.ArtifactId, StringComparison.Ordinal)
 14628            || !string.Equals(canonicalPayload.PayloadSchemaVersion, canonicalHash.PayloadSchemaVersion, StringCompariso
 14629            || !string.Equals(canonicalPayload.CanonicalizationVersion, canonicalHash.CanonicalizationVersion, StringCom
 30        {
 131            throw new ArgumentException("The canonical payload and canonical hash must describe the same governance arti
 32        }
 33
 14534        Artifact = artifact;
 14535        CanonicalPayload = canonicalPayload;
 14536        CanonicalHash = canonicalHash;
 14537        SigningMetadata = signingMetadata;
 14538    }
 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>
 17663    public string ArtifactType => CanonicalHash.ArtifactType;
 64
 65    /// <summary>
 66    /// Gets the stable artifact identifier bound into the canonical payload and signing metadata.
 67    /// </summary>
 14768    public string ArtifactId => CanonicalHash.ArtifactId;
 69
 70    /// <summary>
 71    /// Gets the hash algorithm used to compute <see cref="SigningHash" />.
 72    /// </summary>
 20073    public string HashAlgorithm => CanonicalHash.HashAlgorithm;
 74
 75    /// <summary>
 76    /// Gets the canonical payload hash value that is signed or made signing-ready.
 77    /// </summary>
 17278    public string SigningHash => CanonicalHash.HashValue;
 79
 80    /// <summary>
 81    /// Gets a value indicating whether provider signing metadata includes a signature.
 82    /// </summary>
 1083    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>
 1088    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>
 7093    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    {
 109        return new SignedGovernanceArtifact<TArtifact>(
 110            artifact,
 111            canonicalPayload,
 112            canonicalHash,
 113            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    {
 125        return new SignedGovernanceArtifact<TArtifact>(
 126            artifact,
 127            canonicalPayload,
 128            canonicalHash,
 129            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    {
 152        ArgumentNullException.ThrowIfNull(canonicalPayload);
 153        ArgumentNullException.ThrowIfNull(canonicalHash);
 154
 155        CanonicalPayloadHash recomputedHash = CanonicalPayloadHasher.ComputeHash(canonicalPayload, canonicalHash.HashAlg
 156
 157        return string.Equals(recomputedHash.HashValue, canonicalHash.HashValue, StringComparison.Ordinal)
 158            ? FromSigningMetadata(artifact, canonicalPayload, canonicalHash, signingMetadata)
 159            : throw new ArgumentException(
 160                "The canonical payload does not hash to the supplied canonical hash value.",
 161                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    {
 178        ArgumentNullException.ThrowIfNull(signingMetadata);
 179
 180        return new SignedGovernanceArtifact<TArtifact>(
 181            artifact,
 182            canonicalPayload,
 183            canonicalHash,
 184            MergeCanonicalHashMetadata(canonicalHash, signingMetadata));
 185    }
 186
 187    private static SigningMetadata MergeCanonicalHashMetadata(
 188        CanonicalPayloadHash canonicalHash,
 189        SigningMetadata signingMetadata)
 190    {
 191        if (signingMetadata.SigningHash is not null
 192            && !string.Equals(signingMetadata.SigningHash, canonicalHash.HashValue, StringComparison.Ordinal))
 193        {
 194            throw new ArgumentException("Signing metadata hash must match the canonical payload hash.", nameof(signingMe
 195        }
 196
 197        Dictionary<string, string> metadata = new(StringComparer.Ordinal);
 198
 199        foreach (KeyValuePair<string, string> item in canonicalHash.ToSigningMetadata().Metadata)
 200        {
 201            metadata[item.Key] = item.Value;
 202        }
 203
 204        foreach (KeyValuePair<string, string> item in signingMetadata.Metadata)
 205        {
 206            if (string.IsNullOrWhiteSpace(item.Key))
 207            {
 208                continue;
 209            }
 210
 211            metadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty;
 212        }
 213
 214        return SigningMetadata.Create(
 215            signingHash: canonicalHash.HashValue,
 216            hashAlgorithm: string.IsNullOrWhiteSpace(signingMetadata.HashAlgorithm)
 217                ? canonicalHash.HashAlgorithm
 218                : signingMetadata.HashAlgorithm,
 219            signature: signingMetadata.Signature,
 220            signatureAlgorithm: signingMetadata.SignatureAlgorithm,
 221            keyId: signingMetadata.KeyId,
 222            keyVersion: signingMetadata.KeyVersion,
 223            provider: signingMetadata.Provider,
 224            signedUtc: signingMetadata.SignedUtc,
 225            metadata: metadata);
 226    }
 227}