< 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
87%
Covered lines: 102
Uncovered lines: 15
Coverable lines: 117
Total lines: 264
Line coverage: 87.1%
Branch coverage
64%
Covered branches: 32
Total branches: 50
Branch coverage: 64%
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%4484.61%
VerifyAsync(...)57.14%201469.23%
Dispose()50%2280%
NormalizeKeySize(...)50%22100%
NormalizeRequired(...)50%22100%
NormalizeHashAlgorithm(...)50%44100%
IsSupportedHashAlgorithm(...)100%11100%
ValidateSigningRequest(...)75%131281.81%
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";
 116    private static readonly Encoding SigningEncoding = Encoding.UTF8;
 17
 18    private readonly LocalDevelopmentSigningOptions options;
 19    private readonly RSA rsa;
 20    private bool disposed;
 21
 22    /// <summary>
 23    /// Initializes a new instance of the <see cref="LocalDevelopmentSigningService" /> class with default local-develop
 24    /// </summary>
 25    public LocalDevelopmentSigningService()
 226        : this(LocalDevelopmentSigningOptions.Create())
 27    {
 228    }
 29
 30    /// <summary>
 31    /// Initializes a new instance of the <see cref="LocalDevelopmentSigningService" /> class.
 32    /// </summary>
 533    public LocalDevelopmentSigningService(LocalDevelopmentSigningOptions options)
 34    {
 535        ArgumentNullException.ThrowIfNull(options);
 36
 537        this.options = options;
 538        rsa = RSA.Create();
 539        rsa.KeySize = NormalizeKeySize(options.KeySizeBits);
 540    }
 41
 42    /// <inheritdoc />
 43    public ValueTask<SigningResult> SignAsync(
 44        SigningRequest request,
 45        CancellationToken cancellationToken = default)
 46    {
 547        ArgumentNullException.ThrowIfNull(request);
 548        cancellationToken.ThrowIfCancellationRequested();
 49
 550        string? validationFailure = ValidateSigningRequest(request);
 551        if (validationFailure is not null)
 52        {
 153            return ValueTask.FromResult(CreateUnsignedFailureResult(request, validationFailure, validationFailure));
 54        }
 55
 56        try
 57        {
 458            ThrowIfDisposed();
 59
 460            byte[] data = SigningEncoding.GetBytes(request.SigningHash);
 461            byte[] signature = rsa.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
 62
 463            Dictionary<string, string> metadata = CreateBaseMetadata(request);
 464            metadata["signing_status"] = "signed";
 65
 466            var signingMetadata = SigningMetadata.Create(
 467                signingHash: request.SigningHash,
 468                hashAlgorithm: NormalizeHashAlgorithm(request.HashAlgorithm),
 469                signature: Convert.ToBase64String(signature),
 470                signatureAlgorithm: NormalizeRequired(options.SignatureAlgorithm, LocalDevelopmentSigningOptions.Default
 471                keyId: NormalizeRequired(options.KeyId, LocalDevelopmentSigningOptions.DefaultKeyId),
 472                keyVersion: NormalizeRequired(options.KeyVersion, LocalDevelopmentSigningOptions.DefaultKeyVersion),
 473                provider: NormalizeRequired(options.ProviderName, LocalDevelopmentSigningOptions.DefaultProviderName),
 474                signedUtc: DateTimeOffset.UtcNow,
 475                metadata: metadata);
 76
 477            return ValueTask.FromResult(SigningResult.FromMetadata(signingMetadata));
 78        }
 079        catch (Exception exception) when (exception is CryptographicException or ObjectDisposedException or InvalidOpera
 80        {
 081            if (!options.ReturnUnsignedOnFailure)
 82            {
 083                throw;
 84            }
 85
 086            return ValueTask.FromResult(CreateUnsignedFailureResult(request, "localdev.signing.failed", exception.GetTyp
 87        }
 488    }
 89
 90    /// <inheritdoc />
 91    public ValueTask<SignatureVerificationResult> VerifyAsync(
 92        SignatureVerificationRequest request,
 93        CancellationToken cancellationToken = default)
 94    {
 395        ArgumentNullException.ThrowIfNull(request);
 396        cancellationToken.ThrowIfCancellationRequested();
 97
 398        SigningMetadata metadata = request.SigningMetadata;
 99
 3100        if (!metadata.HasSignature)
 101        {
 0102            return ValueTask.FromResult(SignatureVerificationResult.MissingSignature("The local-development verifier rec
 103        }
 104
 3105        if (!string.Equals(request.SigningHash, metadata.SigningHash, StringComparison.Ordinal))
 106        {
 1107            return ValueTask.FromResult(SignatureVerificationResult.Failed("localdev.signature.hash-mismatch", "The veri
 108        }
 109
 2110        if (!IsSupportedHashAlgorithm(metadata.HashAlgorithm))
 111        {
 0112            return ValueTask.FromResult(SignatureVerificationResult.Failed("localdev.signature.hash-algorithm-unsupporte
 113        }
 114
 2115        if (!string.Equals(metadata.SignatureAlgorithm, NormalizeRequired(options.SignatureAlgorithm, LocalDevelopmentSi
 116        {
 0117            return ValueTask.FromResult(SignatureVerificationResult.Failed("localdev.signature.algorithm-mismatch", "The
 118        }
 119
 2120        if (!string.Equals(metadata.KeyId, NormalizeRequired(options.KeyId, LocalDevelopmentSigningOptions.DefaultKeyId)
 2121            || !string.Equals(metadata.KeyVersion, NormalizeRequired(options.KeyVersion, LocalDevelopmentSigningOptions.
 122        {
 0123            return ValueTask.FromResult(SignatureVerificationResult.Failed("localdev.signature.key-mismatch", "The signa
 124        }
 125
 126        try
 127        {
 2128            ThrowIfDisposed();
 129
 2130            byte[] signature = Convert.FromBase64String(metadata.Signature!);
 2131            byte[] data = SigningEncoding.GetBytes(request.SigningHash);
 2132            bool verified = rsa.VerifyData(data, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
 133
 2134            return ValueTask.FromResult(verified
 2135                ? SignatureVerificationResult.Verified()
 2136                : SignatureVerificationResult.Failed("localdev.signature.invalid", "The local-development signature did 
 137        }
 0138        catch (FormatException)
 139        {
 0140            return ValueTask.FromResult(SignatureVerificationResult.Failed("localdev.signature.malformed", "The signatur
 141        }
 0142        catch (Exception exception) when (exception is CryptographicException or ObjectDisposedException or InvalidOpera
 143        {
 0144            return ValueTask.FromResult(SignatureVerificationResult.Failed("localdev.verification.failed", exception.Get
 145        }
 2146    }
 147
 148    /// <inheritdoc />
 149    public void Dispose()
 150    {
 5151        if (disposed)
 152        {
 0153            return;
 154        }
 155
 5156        rsa.Dispose();
 5157        disposed = true;
 5158    }
 159
 160    private static int NormalizeKeySize(int keySizeBits)
 161    {
 5162        return keySizeBits >= 2048
 5163            ? keySizeBits
 5164            : LocalDevelopmentSigningOptions.DefaultKeySizeBits;
 165    }
 166
 167    private static string NormalizeRequired(string? value, string fallback)
 168    {
 33169        return string.IsNullOrWhiteSpace(value)
 33170            ? fallback
 33171            : value.Trim();
 172    }
 173
 174    private static string NormalizeHashAlgorithm(string? hashAlgorithm)
 175    {
 12176        return string.IsNullOrWhiteSpace(hashAlgorithm)
 12177            ? SupportedHashAlgorithm
 12178            : hashAlgorithm.Trim().Equals("SHA256", StringComparison.OrdinalIgnoreCase)
 12179                ? SupportedHashAlgorithm
 12180                : hashAlgorithm.Trim();
 181    }
 182
 183    private static bool IsSupportedHashAlgorithm(string? hashAlgorithm)
 184    {
 7185        string normalized = NormalizeHashAlgorithm(hashAlgorithm);
 186
 7187        return normalized.Equals(SupportedHashAlgorithm, StringComparison.OrdinalIgnoreCase);
 188    }
 189
 190    private string? ValidateSigningRequest(SigningRequest request)
 191    {
 5192        if (disposed)
 193        {
 0194            return "localdev.signing.disposed";
 195        }
 196
 5197        if (!IsSupportedHashAlgorithm(request.HashAlgorithm))
 198        {
 1199            return "localdev.signing.hash-algorithm-unsupported";
 200        }
 201
 4202        string configuredKeyId = NormalizeRequired(options.KeyId, LocalDevelopmentSigningOptions.DefaultKeyId);
 4203        if (request.KeyId is not null && !string.Equals(request.KeyId, configuredKeyId, StringComparison.Ordinal))
 204        {
 0205            return "localdev.signing.key-mismatch";
 206        }
 207
 4208        string configuredKeyVersion = NormalizeRequired(options.KeyVersion, LocalDevelopmentSigningOptions.DefaultKeyVer
 4209        return request.KeyVersion is not null && !string.Equals(request.KeyVersion, configuredKeyVersion, StringComparis
 4210            ? "localdev.signing.key-version-mismatch"
 4211            : null;
 212    }
 213
 214    private SigningResult CreateUnsignedFailureResult(SigningRequest request, string failureCode, string failureMessage)
 215    {
 1216        Dictionary<string, string> metadata = CreateBaseMetadata(request);
 1217        metadata["signing_status"] = "failed";
 1218        metadata["failure_code"] = failureCode;
 1219        metadata["failure_message"] = failureMessage;
 220
 1221        var signingMetadata = SigningMetadata.Create(
 1222            signingHash: request.SigningHash,
 1223            hashAlgorithm: NormalizeHashAlgorithm(request.HashAlgorithm),
 1224            keyId: NormalizeRequired(options.KeyId, LocalDevelopmentSigningOptions.DefaultKeyId),
 1225            keyVersion: NormalizeRequired(options.KeyVersion, LocalDevelopmentSigningOptions.DefaultKeyVersion),
 1226            provider: NormalizeRequired(options.ProviderName, LocalDevelopmentSigningOptions.DefaultProviderName),
 1227            metadata: metadata);
 228
 1229        return SigningResult.FromMetadata(signingMetadata);
 230    }
 231
 232    private Dictionary<string, string> CreateBaseMetadata(SigningRequest request)
 233    {
 5234        Dictionary<string, string> metadata = new(StringComparer.Ordinal)
 5235        {
 5236            ["provider_kind"] = "local-development",
 5237            ["provider_warning"] = "local-development-only",
 5238            ["key_algorithm"] = "RSA",
 5239            ["key_size_bits"] = rsa.KeySize.ToString(System.Globalization.CultureInfo.InvariantCulture)
 5240        };
 241
 5242        if (request.Purpose is not null)
 243        {
 4244            metadata["purpose"] = request.Purpose;
 245        }
 246
 18247        foreach (KeyValuePair<string, string> item in request.Metadata)
 248        {
 4249            if (string.IsNullOrWhiteSpace(item.Key))
 250            {
 251                continue;
 252            }
 253
 4254            metadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty;
 255        }
 256
 5257        return metadata;
 258    }
 259
 260    private void ThrowIfDisposed()
 261    {
 6262        ObjectDisposedException.ThrowIf(disposed, this);
 6263    }
 264}