< Summary

Information
Class: AsiBackbone.Signing.LocalDevelopment.LocalDevelopmentSigningService
Assembly: AsiBackbone.Signing.LocalDevelopment
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Signing.LocalDevelopment/LocalDevelopmentSigningService.cs
Line coverage
88%
Covered lines: 112
Uncovered lines: 14
Coverable lines: 126
Total lines: 286
Line coverage: 88.8%
Branch coverage
64%
Covered branches: 35
Total branches: 54
Branch coverage: 64.8%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
.ctor(...)100%11100%
.ctor()100%11100%
SignAsync(...)50%4485.18%
VerifyAsync(...)57.14%191470.37%
Dispose()100%22100%
NormalizeRequired(...)50%22100%
NormalizeHashAlgorithm(...)50%44100%
IsSupportedHashAlgorithm(...)100%11100%
FixedTimeHashEquals(...)33.33%6680%
ValidateSigningRequest(...)83.33%121290.9%
CreateUnsignedFailureResult(...)100%11100%
CreateBaseMetadata(...)80%1010100%
ThrowIfDisposed()100%11100%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Signing.LocalDevelopment/LocalDevelopmentSigningService.cs

#LineLine coverage
 1using System.Security.Cryptography;
 2using System.Text;
 3using AsiBackbone.Core.Signing;
 4
 5namespace AsiBackbone.Signing.LocalDevelopment;
 6
 7/// <summary>
 8/// Provides local-development RSA signing and verification for AsiBackbone signing abstractions.
 9/// </summary>
 10/// <remarks>
 11/// This service generates an in-process RSA key for samples and tests. It is not a production managed-key provider.
 12/// </remarks>
 13public sealed class LocalDevelopmentSigningService : IAsiBackboneSigningService, IAsiBackboneSignatureVerificationServic
 14{
 15    private const string SupportedHashAlgorithm = "SHA-256";
 216    private static readonly Encoding SigningEncoding = Encoding.UTF8;
 17
 18    private readonly LocalDevelopmentSigningOptions options;
 19    private readonly RSA rsa;
 3420    private readonly Lock rsaSync = new();
 21    private readonly int keySizeBits;
 22    private bool disposed;
 23
 24    /// <summary>
 25    /// Initializes a new instance of the <see cref="LocalDevelopmentSigningService" /> class with default local-develop
 26    /// </summary>
 27    public LocalDevelopmentSigningService()
 628        : this(LocalDevelopmentSigningOptions.Create())
 29    {
 630    }
 31
 32    /// <summary>
 33    /// Initializes a new instance of the <see cref="LocalDevelopmentSigningService" /> class.
 34    /// </summary>
 35    /// <exception cref="InvalidOperationException">
 36    /// Thrown when the configured RSA key size is below the supported minimum.
 37    /// </exception>
 3438    public LocalDevelopmentSigningService(LocalDevelopmentSigningOptions options)
 39    {
 3440        ArgumentNullException.ThrowIfNull(options);
 3441        options.Validate();
 42
 2443        this.options = options;
 2444        rsa = RSA.Create();
 2445        rsa.KeySize = options.KeySizeBits;
 2446        keySizeBits = rsa.KeySize;
 2447    }
 48
 49    /// <inheritdoc />
 50    public ValueTask<SigningResult> SignAsync(
 51        SigningRequest request,
 52        CancellationToken cancellationToken = default)
 53    {
 5054        ArgumentNullException.ThrowIfNull(request);
 5055        cancellationToken.ThrowIfCancellationRequested();
 56
 5057        string? validationFailure = ValidateSigningRequest(request);
 5058        if (validationFailure is not null)
 59        {
 3460            return ValueTask.FromResult(CreateUnsignedFailureResult(request, validationFailure, validationFailure));
 61        }
 62
 63        try
 1664        {
 65            lock (rsaSync)
 66            {
 1667                ThrowIfDisposed();
 68
 1669                byte[] data = SigningEncoding.GetBytes(request.SigningHash);
 1670                byte[] signature = rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
 71
 1672                Dictionary<string, string> metadata = CreateBaseMetadata(request);
 1673                metadata["signing_status"] = "signed";
 74
 1675                var signingMetadata = SigningMetadata.Create(
 1676                    signingHash: request.SigningHash,
 1677                    hashAlgorithm: NormalizeHashAlgorithm(request.HashAlgorithm),
 1678                    signature: Convert.ToBase64String(signature),
 1679                    signatureAlgorithm: NormalizeRequired(options.SignatureAlgorithm, LocalDevelopmentSigningOptions.Def
 1680                    keyId: NormalizeRequired(options.KeyId, LocalDevelopmentSigningOptions.DefaultKeyId),
 1681                    keyVersion: NormalizeRequired(options.KeyVersion, LocalDevelopmentSigningOptions.DefaultKeyVersion),
 1682                    provider: NormalizeRequired(options.ProviderName, LocalDevelopmentSigningOptions.DefaultProviderName
 1683                    signedUtc: DateTimeOffset.UtcNow,
 1684                    metadata: metadata);
 85
 1686                return ValueTask.FromResult(SigningResult.FromMetadata(signingMetadata));
 87            }
 88        }
 089        catch (Exception exception) when (exception is CryptographicException or ObjectDisposedException or InvalidOpera
 90        {
 091            if (!options.ReturnUnsignedOnFailure)
 92            {
 093                throw;
 94            }
 95
 096            return ValueTask.FromResult(CreateUnsignedFailureResult(request, "localdev.signing.failed", exception.GetTyp
 97        }
 1698    }
 99
 100    /// <inheritdoc />
 101    public ValueTask<SignatureVerificationResult> VerifyAsync(
 102        SignatureVerificationRequest request,
 103        CancellationToken cancellationToken = default)
 104    {
 6105        ArgumentNullException.ThrowIfNull(request);
 6106        cancellationToken.ThrowIfCancellationRequested();
 107
 6108        SigningMetadata metadata = request.SigningMetadata;
 109
 6110        if (!metadata.HasSignature)
 111        {
 0112            return ValueTask.FromResult(SignatureVerificationResult.MissingSignature("The local-development verifier rec
 113        }
 114
 6115        if (!FixedTimeHashEquals(request.SigningHash, metadata.SigningHash))
 116        {
 2117            return ValueTask.FromResult(SignatureVerificationResult.Failed("localdev.signature.hash-mismatch", "The veri
 118        }
 119
 4120        if (!IsSupportedHashAlgorithm(metadata.HashAlgorithm))
 121        {
 0122            return ValueTask.FromResult(SignatureVerificationResult.Failed("localdev.signature.hash-algorithm-unsupporte
 123        }
 124
 4125        if (!string.Equals(metadata.SignatureAlgorithm, NormalizeRequired(options.SignatureAlgorithm, LocalDevelopmentSi
 126        {
 0127            return ValueTask.FromResult(SignatureVerificationResult.Failed("localdev.signature.algorithm-mismatch", "The
 128        }
 129
 4130        if (!string.Equals(metadata.KeyId, NormalizeRequired(options.KeyId, LocalDevelopmentSigningOptions.DefaultKeyId)
 4131            || !string.Equals(metadata.KeyVersion, NormalizeRequired(options.KeyVersion, LocalDevelopmentSigningOptions.
 132        {
 0133            return ValueTask.FromResult(SignatureVerificationResult.Failed("localdev.signature.key-mismatch", "The signa
 134        }
 135
 136        try
 4137        {
 138            lock (rsaSync)
 139            {
 4140                ThrowIfDisposed();
 141
 4142                byte[] signature = Convert.FromBase64String(metadata.Signature!);
 4143                byte[] data = SigningEncoding.GetBytes(request.SigningHash);
 4144                bool verified = rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
 145
 4146                return ValueTask.FromResult(verified
 4147                    ? SignatureVerificationResult.Verified()
 4148                    : SignatureVerificationResult.Failed("localdev.signature.invalid", "The local-development signature 
 149            }
 150        }
 0151        catch (FormatException)
 152        {
 0153            return ValueTask.FromResult(SignatureVerificationResult.Failed("localdev.signature.malformed", "The signatur
 154        }
 0155        catch (Exception exception) when (exception is CryptographicException or ObjectDisposedException or InvalidOpera
 156        {
 0157            return ValueTask.FromResult(SignatureVerificationResult.Failed("localdev.verification.failed", exception.Get
 158        }
 4159    }
 160
 161    /// <inheritdoc />
 162    public void Dispose()
 66163    {
 164        lock (rsaSync)
 165        {
 66166            if (disposed)
 167            {
 42168                return;
 169            }
 170
 24171            disposed = true;
 24172            rsa.Dispose();
 24173        }
 66174    }
 175
 176    private static string NormalizeRequired(string? value, string fallback)
 177    {
 210178        return string.IsNullOrWhiteSpace(value)
 210179            ? fallback
 210180            : value.Trim();
 181    }
 182
 183    private static string NormalizeHashAlgorithm(string? hashAlgorithm)
 184    {
 72185        return string.IsNullOrWhiteSpace(hashAlgorithm)
 72186            ? SupportedHashAlgorithm
 72187            : hashAlgorithm.Trim().Equals("SHA256", StringComparison.OrdinalIgnoreCase)
 72188                ? SupportedHashAlgorithm
 72189                : hashAlgorithm.Trim();
 190    }
 191
 192    private static bool IsSupportedHashAlgorithm(string? hashAlgorithm)
 193    {
 22194        string normalized = NormalizeHashAlgorithm(hashAlgorithm);
 195
 22196        return normalized.Equals(SupportedHashAlgorithm, StringComparison.OrdinalIgnoreCase);
 197    }
 198
 199    private static bool FixedTimeHashEquals(string? expectedHash, string? actualHash)
 200    {
 6201        if (expectedHash is null || actualHash is null)
 202        {
 0203            return expectedHash is null && actualHash is null;
 204        }
 205
 6206        byte[] expectedBytes = SigningEncoding.GetBytes(expectedHash);
 6207        byte[] actualBytes = SigningEncoding.GetBytes(actualHash);
 208
 6209        return CryptographicOperations.FixedTimeEquals(expectedBytes, actualBytes);
 210    }
 211
 212    private string? ValidateSigningRequest(SigningRequest request)
 213    {
 50214        if (Volatile.Read(ref disposed))
 215        {
 32216            return "localdev.signing.disposed";
 217        }
 218
 18219        if (!IsSupportedHashAlgorithm(request.HashAlgorithm))
 220        {
 2221            return "localdev.signing.hash-algorithm-unsupported";
 222        }
 223
 16224        string configuredKeyId = NormalizeRequired(options.KeyId, LocalDevelopmentSigningOptions.DefaultKeyId);
 16225        if (request.KeyId is not null && !string.Equals(request.KeyId, configuredKeyId, StringComparison.Ordinal))
 226        {
 0227            return "localdev.signing.key-mismatch";
 228        }
 229
 16230        string configuredKeyVersion = NormalizeRequired(options.KeyVersion, LocalDevelopmentSigningOptions.DefaultKeyVer
 16231        return request.KeyVersion is not null && !string.Equals(request.KeyVersion, configuredKeyVersion, StringComparis
 16232            ? "localdev.signing.key-version-mismatch"
 16233            : null;
 234    }
 235
 236    private SigningResult CreateUnsignedFailureResult(SigningRequest request, string failureCode, string failureMessage)
 237    {
 34238        Dictionary<string, string> metadata = CreateBaseMetadata(request);
 34239        metadata["signing_status"] = "failed";
 34240        metadata["failure_code"] = failureCode;
 34241        metadata["failure_message"] = failureMessage;
 242
 34243        var signingMetadata = SigningMetadata.Create(
 34244            signingHash: request.SigningHash,
 34245            hashAlgorithm: NormalizeHashAlgorithm(request.HashAlgorithm),
 34246            keyId: NormalizeRequired(options.KeyId, LocalDevelopmentSigningOptions.DefaultKeyId),
 34247            keyVersion: NormalizeRequired(options.KeyVersion, LocalDevelopmentSigningOptions.DefaultKeyVersion),
 34248            provider: NormalizeRequired(options.ProviderName, LocalDevelopmentSigningOptions.DefaultProviderName),
 34249            metadata: metadata);
 250
 34251        return SigningResult.FromMetadata(signingMetadata);
 252    }
 253
 254    private Dictionary<string, string> CreateBaseMetadata(SigningRequest request)
 255    {
 50256        Dictionary<string, string> metadata = new(StringComparer.Ordinal)
 50257        {
 50258            ["provider_kind"] = "local-development",
 50259            ["provider_warning"] = "local-development-only",
 50260            ["key_algorithm"] = "RSA",
 50261            ["key_size_bits"] = keySizeBits.ToString(System.Globalization.CultureInfo.InvariantCulture)
 50262        };
 263
 50264        if (request.Purpose is not null)
 265        {
 8266            metadata["purpose"] = request.Purpose;
 267        }
 268
 116269        foreach (KeyValuePair<string, string> item in request.Metadata)
 270        {
 8271            if (string.IsNullOrWhiteSpace(item.Key))
 272            {
 273                continue;
 274            }
 275
 8276            metadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty;
 277        }
 278
 50279        return metadata;
 280    }
 281
 282    private void ThrowIfDisposed()
 283    {
 20284        ObjectDisposedException.ThrowIf(disposed, this);
 20285    }
 286}