< Summary

Information
Class: AsiBackbone.Signing.ManagedKey.ManagedKeySigningService
Assembly: AsiBackbone.Signing.ManagedKey
File(s): /home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Signing.ManagedKey/ManagedKeySigningService.cs
Line coverage
89%
Covered lines: 110
Uncovered lines: 13
Coverable lines: 123
Total lines: 278
Line coverage: 89.4%
Branch coverage
68%
Covered branches: 51
Total branches: 74
Branch coverage: 68.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
SignAsync()100%2265.21%
HandleFailure(...)0%620%
CreateManagedKeyRequest(...)100%11100%
CreateSignedResult(...)56.25%1616100%
CreateUnsignedFailureResult(...)100%11100%
CreateBaseMetadata(...)70%1010100%
ValidateSigningRequest(...)66.66%181892.3%
ResolveKeyId(...)50%22100%
ResolveKeyVersion(...)100%22100%
NormalizeHashAlgorithm(...)66.66%66100%
IsSupportedHashAlgorithm(...)100%11100%
DelayForRetryAsync()50%2266.66%
IsSafeProviderMetadataKey(...)100%1010100%
NormalizeRequired(...)50%22100%
NormalizeOptional(...)100%22100%

File(s)

/home/runner/work/AsiBackbone/AsiBackbone/src/AsiBackbone.Signing.ManagedKey/ManagedKeySigningService.cs

#LineLine coverage
 1using System.Runtime.ExceptionServices;
 2using AsiBackbone.Core.Signing;
 3
 4namespace AsiBackbone.Signing.ManagedKey;
 5
 6/// <summary>
 7/// Provides managed-key signing for AsiBackbone signing abstractions through a host-owned managed-key client.
 8/// </summary>
 9/// <remarks>
 10/// This service signs precomputed hashes only. It never requests or handles raw private key material.
 11/// </remarks>
 12public sealed class ManagedKeySigningService : IAsiBackboneSigningService
 13{
 14    private const string ProviderKind = "managed-key";
 15
 16    private readonly ManagedKeySigningOptions options;
 17    private readonly IManagedKeySigningClient client;
 18
 19    /// <summary>
 20    /// Initializes a new instance of the <see cref="ManagedKeySigningService" /> class.
 21    /// </summary>
 1022    public ManagedKeySigningService(ManagedKeySigningOptions options, IManagedKeySigningClient client)
 23    {
 1024        ArgumentNullException.ThrowIfNull(options);
 1025        ArgumentNullException.ThrowIfNull(client);
 26
 1027        options.Validate();
 1028        this.options = options;
 1029        this.client = client;
 1030    }
 31
 32    /// <inheritdoc />
 33    public async ValueTask<SigningResult> SignAsync(
 34        SigningRequest request,
 35        CancellationToken cancellationToken = default)
 36    {
 837        ArgumentNullException.ThrowIfNull(request);
 838        cancellationToken.ThrowIfCancellationRequested();
 39
 840        string? validationFailure = ValidateSigningRequest(request);
 841        if (validationFailure is not null)
 42        {
 443            return CreateUnsignedFailureResult(request, validationFailure, validationFailure, retryAttempts: 0);
 44        }
 45
 446        int attempt = 0;
 47
 48        while (true)
 49        {
 50            try
 51            {
 652                ManagedKeySignResult managedResult = await client
 653                    .SignAsync(CreateManagedKeyRequest(request), cancellationToken)
 654                    .ConfigureAwait(false);
 55
 456                return CreateSignedResult(request, managedResult, attempt);
 57            }
 258            catch (ManagedKeySigningException exception) when (exception.IsRetryable && attempt < options.MaxRetryAttemp
 59            {
 260                attempt++;
 261                await DelayForRetryAsync(cancellationToken).ConfigureAwait(false);
 262            }
 063            catch (ManagedKeySigningException exception)
 64            {
 065                return HandleFailure(request, exception.FailureCode, exception.Message, attempt, exception);
 66            }
 067            catch (TimeoutException exception)
 68            {
 069                return HandleFailure(request, "managedkey.signing.provider-unavailable", exception.GetType().Name, attem
 70            }
 071            catch (InvalidOperationException exception)
 72            {
 073                return HandleFailure(request, "managedkey.signing.failed", exception.GetType().Name, attempt, exception)
 74            }
 075            catch (NotSupportedException exception)
 76            {
 077                return HandleFailure(request, "managedkey.signing.unsupported", exception.GetType().Name, attempt, excep
 78            }
 79        }
 880    }
 81
 82    private SigningResult HandleFailure(
 83        SigningRequest request,
 84        string failureCode,
 85        string failureMessage,
 86        int retryAttempts,
 87        Exception exception)
 88    {
 089        if (!options.ReturnUnsignedOnFailure)
 90        {
 091            ExceptionDispatchInfo.Capture(exception).Throw();
 92        }
 93
 094        return CreateUnsignedFailureResult(request, failureCode, failureMessage, retryAttempts);
 95    }
 96
 97    private ManagedKeySignRequest CreateManagedKeyRequest(SigningRequest request)
 98    {
 699        return new ManagedKeySignRequest(
 6100            request.SigningHash,
 6101            NormalizeHashAlgorithm(request.HashAlgorithm),
 6102            NormalizeRequired(options.SignatureAlgorithm, ManagedKeySigningOptions.DefaultSignatureAlgorithm),
 6103            ResolveKeyId(request),
 6104            ResolveKeyVersion(request),
 6105            request.Purpose,
 6106            request.Metadata);
 107    }
 108
 109    private SigningResult CreateSignedResult(
 110        SigningRequest request,
 111        ManagedKeySignResult managedResult,
 112        int retryAttempts)
 113    {
 4114        string resolvedKeyVersion = managedResult.KeyVersion ?? ResolveKeyVersion(request) ?? string.Empty;
 4115        Dictionary<string, string> metadata = CreateBaseMetadata(request, retryAttempts);
 4116        metadata["signing_status"] = "signed";
 117
 4118        if (managedResult.ProviderOperationId is not null)
 119        {
 4120            metadata["provider_operation_id"] = managedResult.ProviderOperationId;
 121        }
 122
 24123        foreach (KeyValuePair<string, string> item in managedResult.Metadata)
 124        {
 8125            if (!IsSafeProviderMetadataKey(item.Key))
 126            {
 127                continue;
 128            }
 129
 4130            metadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty;
 131        }
 132
 4133        var signingMetadata = SigningMetadata.Create(
 4134            signingHash: request.SigningHash,
 4135            hashAlgorithm: NormalizeHashAlgorithm(request.HashAlgorithm),
 4136            signature: managedResult.Signature,
 4137            signatureAlgorithm: managedResult.SignatureAlgorithm,
 4138            keyId: managedResult.KeyId,
 4139            keyVersion: string.IsNullOrWhiteSpace(resolvedKeyVersion) ? null : resolvedKeyVersion,
 4140            provider: NormalizeRequired(options.ProviderName, ManagedKeySigningOptions.DefaultProviderName),
 4141            signedUtc: managedResult.SignedUtc,
 4142            metadata: metadata);
 143
 4144        return SigningResult.FromMetadata(signingMetadata);
 145    }
 146
 147    private SigningResult CreateUnsignedFailureResult(
 148        SigningRequest request,
 149        string failureCode,
 150        string failureMessage,
 151        int retryAttempts)
 152    {
 4153        Dictionary<string, string> metadata = CreateBaseMetadata(request, retryAttempts);
 4154        metadata["signing_status"] = "failed";
 4155        metadata["failure_code"] = failureCode;
 4156        metadata["failure_message"] = failureMessage;
 157
 4158        var signingMetadata = SigningMetadata.Create(
 4159            signingHash: request.SigningHash,
 4160            hashAlgorithm: NormalizeHashAlgorithm(request.HashAlgorithm),
 4161            keyId: ResolveKeyId(request),
 4162            keyVersion: ResolveKeyVersion(request),
 4163            provider: NormalizeRequired(options.ProviderName, ManagedKeySigningOptions.DefaultProviderName),
 4164            metadata: metadata);
 165
 4166        return SigningResult.FromMetadata(signingMetadata);
 167    }
 168
 169    private Dictionary<string, string> CreateBaseMetadata(SigningRequest request, int retryAttempts)
 170    {
 8171        Dictionary<string, string> metadata = new(StringComparer.Ordinal)
 8172        {
 8173            ["provider_kind"] = ProviderKind,
 8174            ["remote_key_material"] = "true",
 8175            ["raw_private_key_loaded"] = "false",
 8176            ["retry_attempts"] = retryAttempts.ToString(System.Globalization.CultureInfo.InvariantCulture),
 8177            ["signature_algorithm"] = NormalizeRequired(options.SignatureAlgorithm, ManagedKeySigningOptions.DefaultSign
 8178        };
 179
 8180        if (request.Purpose is not null)
 181        {
 8182            metadata["purpose"] = request.Purpose;
 183        }
 184
 24185        foreach (KeyValuePair<string, string> item in request.Metadata)
 186        {
 4187            if (string.IsNullOrWhiteSpace(item.Key))
 188            {
 189                continue;
 190            }
 191
 4192            metadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty;
 193        }
 194
 8195        return metadata;
 196    }
 197
 198    private string? ValidateSigningRequest(SigningRequest request)
 199    {
 8200        if (!IsSupportedHashAlgorithm(request.HashAlgorithm))
 201        {
 2202            return "managedkey.signing.hash-algorithm-unsupported";
 203        }
 204
 6205        string configuredKeyId = NormalizeRequired(options.KeyId, string.Empty);
 6206        if (request.KeyId is not null && !string.Equals(request.KeyId, configuredKeyId, StringComparison.Ordinal))
 207        {
 0208            return "managedkey.signing.key-mismatch";
 209        }
 210
 6211        string? configuredKeyVersion = NormalizeOptional(options.KeyVersion);
 6212        return options.RequireKeyVersion && request.KeyVersion is null && configuredKeyVersion is null
 6213            ? "managedkey.signing.key-version-missing"
 6214            : request.KeyVersion is not null
 6215            && configuredKeyVersion is not null
 6216            && !string.Equals(request.KeyVersion, configuredKeyVersion, StringComparison.Ordinal)
 6217            ? "managedkey.signing.key-version-mismatch"
 6218            : null;
 219    }
 220
 221    private string ResolveKeyId(SigningRequest request)
 222    {
 10223        return request.KeyId ?? NormalizeRequired(options.KeyId, string.Empty);
 224    }
 225
 226    private string? ResolveKeyVersion(SigningRequest request)
 227    {
 10228        return request.KeyVersion ?? NormalizeOptional(options.KeyVersion);
 229    }
 230
 231    private string NormalizeHashAlgorithm(string? hashAlgorithm)
 232    {
 30233        string normalized = string.IsNullOrWhiteSpace(hashAlgorithm)
 30234            ? options.HashAlgorithm
 30235            : hashAlgorithm.Trim();
 236
 30237        return normalized.Equals("SHA256", StringComparison.OrdinalIgnoreCase)
 30238            ? ManagedKeySigningOptions.DefaultHashAlgorithm
 30239            : normalized.Equals(ManagedKeySigningOptions.DefaultHashAlgorithm, StringComparison.OrdinalIgnoreCase)
 30240                ? ManagedKeySigningOptions.DefaultHashAlgorithm
 30241                : normalized;
 242    }
 243
 244    private bool IsSupportedHashAlgorithm(string? hashAlgorithm)
 245    {
 8246        return NormalizeHashAlgorithm(hashAlgorithm).Equals(
 8247            NormalizeHashAlgorithm(options.HashAlgorithm),
 8248            StringComparison.OrdinalIgnoreCase);
 249    }
 250
 251    private async ValueTask DelayForRetryAsync(CancellationToken cancellationToken)
 252    {
 2253        if (options.RetryDelay > TimeSpan.Zero)
 254        {
 0255            await Task.Delay(options.RetryDelay, cancellationToken).ConfigureAwait(false);
 256        }
 2257    }
 258
 259    private static bool IsSafeProviderMetadataKey(string key)
 260    {
 8261        return !string.IsNullOrWhiteSpace(key)
 8262            && !key.Contains("secret", StringComparison.OrdinalIgnoreCase)
 8263            && !key.Contains("token", StringComparison.OrdinalIgnoreCase)
 8264            && !key.Contains("credential", StringComparison.OrdinalIgnoreCase)
 8265            && !key.Contains("private", StringComparison.OrdinalIgnoreCase)
 8266            && !key.Contains("connection", StringComparison.OrdinalIgnoreCase);
 267    }
 268
 269    private static string NormalizeRequired(string? value, string fallback)
 270    {
 28271        return string.IsNullOrWhiteSpace(value) ? fallback : value.Trim();
 272    }
 273
 274    private static string? NormalizeOptional(string? value)
 275    {
 8276        return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
 277    }
 278}