< Summary

Information
Class: AsiBackbone.Core.Signing.CanonicalPayloadJson
Assembly: AsiBackbone.Core
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Core/Signing/CanonicalPayload.cs
Line coverage
100%
Covered lines: 50
Uncovered lines: 0
Coverable lines: 50
Total lines: 227
Line coverage: 100%
Branch coverage
100%
Covered branches: 32
Total branches: 32
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
Serialize(...)100%11100%
WriteValue(...)100%2828100%
WriteDictionary(...)100%22100%
WriteStringDictionary(...)100%22100%

File(s)

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

#LineLine coverage
 1using System.Buffers;
 2using System.Text;
 3using System.Text.Json;
 4
 5namespace AsiBackbone.Core.Signing;
 6
 7/// <summary>
 8/// Represents a deterministic, provider-neutral payload that can be hashed and later signed by a host or provider packa
 9/// </summary>
 10public sealed class CanonicalPayload
 11{
 12    private CanonicalPayload(
 13        string artifactType,
 14        string artifactId,
 15        string payloadSchemaVersion,
 16        string canonicalizationVersion,
 17        string hashAlgorithm,
 18        string canonicalJson)
 19    {
 20        ArtifactType = artifactType;
 21        ArtifactId = artifactId;
 22        PayloadSchemaVersion = payloadSchemaVersion;
 23        CanonicalizationVersion = canonicalizationVersion;
 24        HashAlgorithm = hashAlgorithm;
 25        CanonicalJson = canonicalJson;
 26    }
 27
 28    /// <summary>
 29    /// Gets the stable artifact type bound into the canonical payload.
 30    /// </summary>
 31    public string ArtifactType { get; }
 32
 33    /// <summary>
 34    /// Gets the stable artifact identifier bound into the canonical payload.
 35    /// </summary>
 36    public string ArtifactId { get; }
 37
 38    /// <summary>
 39    /// Gets the payload schema version bound into the canonical payload.
 40    /// </summary>
 41    public string PayloadSchemaVersion { get; }
 42
 43    /// <summary>
 44    /// Gets the canonicalization version used to create the payload bytes.
 45    /// </summary>
 46    public string CanonicalizationVersion { get; }
 47
 48    /// <summary>
 49    /// Gets the hash algorithm selected for the canonical payload bytes.
 50    /// </summary>
 51    public string HashAlgorithm { get; }
 52
 53    /// <summary>
 54    /// Gets the deterministic JSON payload to hash or sign.
 55    /// </summary>
 56    public string CanonicalJson { get; }
 57
 58    /// <summary>
 59    /// Creates a canonical payload envelope around deterministic artifact content.
 60    /// </summary>
 61    public static CanonicalPayload Create(
 62        string artifactType,
 63        string artifactId,
 64        string payloadSchemaVersion,
 65        string canonicalizationVersion,
 66        IReadOnlyDictionary<string, object?> content)
 67    {
 68        return Create(
 69            artifactType,
 70            artifactId,
 71            payloadSchemaVersion,
 72            canonicalizationVersion,
 73            CanonicalPayloadOptions.DefaultHashAlgorithm,
 74            content);
 75    }
 76
 77    /// <summary>
 78    /// Creates a canonical payload envelope around deterministic artifact content with a selected hash algorithm.
 79    /// </summary>
 80    public static CanonicalPayload Create(
 81        string artifactType,
 82        string artifactId,
 83        string payloadSchemaVersion,
 84        string canonicalizationVersion,
 85        string hashAlgorithm,
 86        IReadOnlyDictionary<string, object?> content)
 87    {
 88        ArgumentException.ThrowIfNullOrWhiteSpace(artifactType);
 89        ArgumentException.ThrowIfNullOrWhiteSpace(artifactId);
 90        ArgumentException.ThrowIfNullOrWhiteSpace(payloadSchemaVersion);
 91        ArgumentException.ThrowIfNullOrWhiteSpace(canonicalizationVersion);
 92        ArgumentException.ThrowIfNullOrWhiteSpace(hashAlgorithm);
 93        ArgumentNullException.ThrowIfNull(content);
 94
 95        string normalizedArtifactType = artifactType.Trim();
 96        string normalizedArtifactId = artifactId.Trim();
 97        string normalizedPayloadSchemaVersion = payloadSchemaVersion.Trim();
 98        string normalizedCanonicalizationVersion = canonicalizationVersion.Trim();
 99        string normalizedHashAlgorithm = CanonicalPayloadHash.NormalizeHashAlgorithm(hashAlgorithm);
 100
 101        SortedDictionary<string, object?> envelope = new(StringComparer.Ordinal)
 102        {
 103            ["artifactId"] = normalizedArtifactId,
 104            ["artifactType"] = normalizedArtifactType,
 105            ["canonicalizationVersion"] = normalizedCanonicalizationVersion,
 106            ["content"] = content,
 107            ["payloadSchemaVersion"] = normalizedPayloadSchemaVersion
 108        };
 109
 110        return new CanonicalPayload(
 111            normalizedArtifactType,
 112            normalizedArtifactId,
 113            normalizedPayloadSchemaVersion,
 114            normalizedCanonicalizationVersion,
 115            normalizedHashAlgorithm,
 116            CanonicalPayloadJson.Serialize(envelope));
 117    }
 118
 119    /// <summary>
 120    /// Gets the canonical JSON payload as UTF-8 bytes.
 121    /// </summary>
 122    public byte[] ToUtf8Bytes()
 123    {
 124        return Encoding.UTF8.GetBytes(CanonicalJson);
 125    }
 126}
 127
 128internal static class CanonicalPayloadJson
 129{
 3130    private static readonly JsonWriterOptions WriterOptions = new()
 3131    {
 3132        Indented = false,
 3133        SkipValidation = false
 3134    };
 135
 136    public static string Serialize(IReadOnlyDictionary<string, object?> value)
 137    {
 564138        ArrayBufferWriter<byte> buffer = new();
 564139        using (Utf8JsonWriter writer = new(buffer, WriterOptions))
 140        {
 564141            WriteDictionary(writer, value);
 560142        }
 143
 560144        return Encoding.UTF8.GetString(buffer.WrittenSpan);
 145    }
 146
 147    private static void WriteValue(Utf8JsonWriter writer, object? value)
 148    {
 149        switch (value)
 150        {
 151            case null:
 2790152                writer.WriteNullValue();
 2790153                break;
 154            case string stringValue:
 6689155                writer.WriteStringValue(stringValue);
 6689156                break;
 157            case bool boolValue:
 12158                writer.WriteBooleanValue(boolValue);
 12159                break;
 160            case int intValue:
 108161                writer.WriteNumberValue(intValue);
 108162                break;
 163            case long longValue:
 140164                writer.WriteNumberValue(longValue);
 140165                break;
 166            case double doubleValue:
 54167                if (double.IsNaN(doubleValue) || double.IsInfinity(doubleValue))
 168                {
 3169                    throw new ArgumentOutOfRangeException(nameof(value), value, "Canonical payload numbers must be finit
 170                }
 171
 51172                writer.WriteNumberValue(doubleValue);
 51173                break;
 174            case IReadOnlyDictionary<string, object?> dictionaryValue:
 885175                WriteDictionary(writer, dictionaryValue);
 881176                break;
 177            case IReadOnlyDictionary<string, string> stringDictionaryValue:
 1178                WriteStringDictionary(writer, stringDictionaryValue);
 1179                break;
 180            case IEnumerable<string> stringValues:
 228181                writer.WriteStartArray();
 882182                foreach (string item in stringValues)
 183                {
 213184                    writer.WriteStringValue(item);
 185                }
 186
 228187                writer.WriteEndArray();
 228188                break;
 189            case IEnumerable<object?> objectValues:
 2190                writer.WriteStartArray();
 18191                foreach (object? item in objectValues)
 192                {
 7193                    WriteValue(writer, item);
 194                }
 195
 2196                writer.WriteEndArray();
 2197                break;
 198            default:
 1199                throw new NotSupportedException($"Canonical payload value type '{value.GetType().FullName}' is not suppo
 200        }
 201    }
 202
 203    private static void WriteDictionary(Utf8JsonWriter writer, IReadOnlyDictionary<string, object?> dictionary)
 204    {
 1449205        writer.WriteStartObject();
 206
 24696207        foreach (KeyValuePair<string, object?> item in dictionary.OrderBy(item => item.Key, StringComparer.Ordinal))
 208        {
 10903209            writer.WritePropertyName(item.Key);
 10903210            WriteValue(writer, item.Value);
 211        }
 212
 1441213        writer.WriteEndObject();
 1441214    }
 215
 216    private static void WriteStringDictionary(Utf8JsonWriter writer, IReadOnlyDictionary<string, string> dictionary)
 217    {
 1218        writer.WriteStartObject();
 219
 6220        foreach (KeyValuePair<string, string> item in dictionary.OrderBy(item => item.Key, StringComparer.Ordinal))
 221        {
 2222            writer.WriteString(item.Key, item.Value);
 223        }
 224
 1225        writer.WriteEndObject();
 1226    }
 227}