< Summary

Information
Class: AsiBackbone.Core.Signing.GovernanceSignatureInput
Assembly: AsiBackbone.Core
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Signing/GovernanceSignatureInput.cs
Line coverage
100%
Covered lines: 21
Uncovered lines: 0
Coverable lines: 21
Total lines: 106
Line coverage: 100%
Branch coverage
100%
Covered branches: 6
Total branches: 6
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
CreateV1(...)100%11100%
CreateLegacy(...)100%11100%
GetBoundValue(...)100%66100%

File(s)

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

#LineLine coverage
 1using System.Text;
 2
 3namespace AsiBackbone.Core.Signing;
 4
 5/// <summary>
 6/// Builds the exact bytes a signing provider signs and a verification provider verifies for a governance artifact.
 7/// </summary>
 8/// <remarks>
 9/// <para>
 10/// Before 6.0, providers signed only the UTF-8 text of the canonical payload hash. Every other value recorded in
 11/// <see cref="SigningMetadata" />, including the signing policy version and policy hash, was an unauthenticated label: 
 12/// holder of a validly signed artifact could relabel it and verification still succeeded.
 13/// </para>
 14/// <para>
 15/// The version 1 signature input is a canonical JSON document that binds the format identifier, the canonical artifact
 16/// descriptors, the hash algorithm, the hash value, and the signing policy context recorded under
 17/// <see cref="PolicyVersionMetadataKey" /> and <see cref="PolicyHashMetadataKey" />. An absent policy value is bound as
 18/// JSON <c>null</c>, so adding, removing, or changing a policy label after signing invalidates the signature.
 19/// </para>
 20/// <para>
 21/// Key identifier, key version, provider, and signing timestamp are not part of the input, because managed-key provider
 22/// commonly resolve the key version and timestamp during signing. They are authenticated only to the extent that the
 23/// verification service resolves its verification key from <see cref="SigningMetadata.KeyId" /> and
 24/// <see cref="SigningMetadata.KeyVersion" /> and rejects a <see cref="SigningMetadata.Provider" /> it does not own.
 25/// </para>
 26/// </remarks>
 27public static class GovernanceSignatureInput
 28{
 29    /// <summary>
 30    /// Identifies the version 1 signature input format.
 31    /// </summary>
 32    public const string FormatV1 = "asibackbone.signature-input.v1";
 33
 34    /// <summary>
 35    /// The signing metadata key whose value is bound into the version 1 signature input as the signing policy version.
 36    /// </summary>
 37    public const string PolicyVersionMetadataKey = "policy_version";
 38
 39    /// <summary>
 40    /// The signing metadata key whose value is bound into the version 1 signature input as the signing policy hash.
 41    /// </summary>
 42    public const string PolicyHashMetadataKey = "policy_hash";
 43
 44    /// <summary>
 45    /// Creates the version 1 signature input for a canonical payload hash and the signing policy context in the supplie
 46    /// signing metadata.
 47    /// </summary>
 48    /// <param name="canonicalHash">The canonical payload hash being signed or verified.</param>
 49    /// <param name="signingMetadata">
 50    /// Signing metadata supplying the <see cref="PolicyVersionMetadataKey" /> and <see cref="PolicyHashMetadataKey" />
 51    /// values. Missing, empty, or whitespace-only values are bound as JSON <c>null</c>; other values are trimmed.
 52    /// </param>
 53    /// <returns>The UTF-8 canonical JSON bytes to sign or verify.</returns>
 54    public static ReadOnlyMemory<byte> CreateV1(
 55        CanonicalPayloadHash canonicalHash,
 56        IReadOnlyDictionary<string, string>? signingMetadata = null)
 57    {
 6758        ArgumentNullException.ThrowIfNull(canonicalHash);
 59
 6760        SortedDictionary<string, object?> input = new(StringComparer.Ordinal)
 6761        {
 6762            ["artifactId"] = canonicalHash.ArtifactId,
 6763            ["artifactType"] = canonicalHash.ArtifactType,
 6764            ["canonicalizationVersion"] = canonicalHash.CanonicalizationVersion,
 6765            ["format"] = FormatV1,
 6766            ["hashAlgorithm"] = canonicalHash.HashAlgorithm,
 6767            ["hashValue"] = canonicalHash.HashValue,
 6768            ["payloadSchemaVersion"] = canonicalHash.PayloadSchemaVersion,
 6769            ["policyHash"] = GetBoundValue(signingMetadata, PolicyHashMetadataKey),
 6770            ["policyVersion"] = GetBoundValue(signingMetadata, PolicyVersionMetadataKey)
 6771        };
 72
 6773        return Encoding.UTF8.GetBytes(CanonicalPayloadJson.Serialize(input));
 74    }
 75
 76    /// <summary>
 77    /// Creates the pre-6.0 signature input: the UTF-8 text of the signing hash alone.
 78    /// </summary>
 79    /// <remarks>
 80    /// This input authenticates the canonical payload hash only. Core uses it for artifacts signed before 6.0 when a
 81    /// verification context explicitly opts in through
 82    /// <see cref="VerificationPolicyContext.WithLegacySignatureInputAllowed" />, and as the fallback for provider reque
 83    /// constructed without an explicit signature input.
 84    /// </remarks>
 85    /// <param name="signingHash">The canonical payload hash value.</param>
 86    /// <returns>The UTF-8 bytes of the trimmed signing hash.</returns>
 87    [Obsolete(
 88        "CreateLegacy produces the pre-6.0 hash-only signature input and is retained only for migration compatibility. U
 89        DiagnosticId = "ASIB902",
 90        UrlFormat = "https://asibackbone.github.io/AsiBackbone/articles/asib902-legacy-signature-input.html")]
 91    public static ReadOnlyMemory<byte> CreateLegacy(string signingHash)
 92    {
 5493        ArgumentException.ThrowIfNullOrWhiteSpace(signingHash);
 94
 5495        return Encoding.UTF8.GetBytes(signingHash.Trim());
 96    }
 97
 98    private static string? GetBoundValue(IReadOnlyDictionary<string, string>? metadata, string key)
 99    {
 134100        return metadata is not null
 134101            && metadata.TryGetValue(key, out string? value)
 134102            && !string.IsNullOrWhiteSpace(value)
 134103            ? value.Trim()
 134104            : null;
 105    }
 106}