< 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
80%
Covered lines: 136
Uncovered lines: 33
Coverable lines: 169
Total lines: 382
Line coverage: 80.4%
Branch coverage
68%
Covered branches: 41
Total branches: 60
Branch coverage: 68.3%
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%
.ctor(...)100%11100%
SignAsync(...)66.67%6686.21%
VerifyAsync(...)68.75%381655.77%
Dispose()100%22100%
CreateRsa(...)50%2266.67%
NormalizeRequired(...)50%22100%
NormalizeHashAlgorithm(...)50%44100%
IsSupportedHashAlgorithm(...)100%11100%
FixedTimeHashEquals(...)33.33%6680%
ValidateSigningRequest(...)83.33%121290.91%
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 : IGovernanceSigningService, IGovernanceSignatureVerificationService,
 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 TimeProvider timeProvider;
 20    private readonly RSA rsa;
 2721    private readonly Lock rsaSync = new();
 22    private readonly int keySizeBits;
 23    private bool disposed;
 24
 25    /// <summary>
 26    /// Initializes a new instance of the <see cref="LocalDevelopmentSigningService" /> class with default local-develop
 27    /// </summary>
 28    public LocalDevelopmentSigningService()
 629        : this(LocalDevelopmentSigningOptions.Create())
 30    {
 631    }
 32
 33    /// <summary>
 34    /// Initializes a new instance of the <see cref="LocalDevelopmentSigningService" /> class.
 35    /// </summary>
 36    /// <exception cref="InvalidOperationException">
 37    /// Thrown when the configured RSA key size is below the supported minimum or is not supported by the platform RSA
 38    /// provider.
 39    /// </exception>
 40    public LocalDevelopmentSigningService(LocalDevelopmentSigningOptions options)
 2241        : this(options, TimeProvider.System)
 42    {
 1543    }
 44
 45    /// <summary>
 46    /// Initializes a new instance of the <see cref="LocalDevelopmentSigningService" /> class with an explicit clock.
 47    /// </summary>
 48    /// <param name="options">The local-development signing options. A snapshot is taken, so later changes to this insta
 49    /// <param name="timeProvider">The clock used to record the signing time in signing metadata.</param>
 50    /// <exception cref="InvalidOperationException">
 51    /// Thrown when the configured RSA key size is below the supported minimum or is not supported by the platform RSA
 52    /// provider.
 53    /// </exception>
 2754    public LocalDevelopmentSigningService(LocalDevelopmentSigningOptions options, TimeProvider timeProvider)
 55    {
 2756        ArgumentNullException.ThrowIfNull(options);
 2757        ArgumentNullException.ThrowIfNull(timeProvider);
 58
 59        // Validate the snapshot rather than the caller's instance, so the values that were checked are the values used.
 2760        LocalDevelopmentSigningOptions snapshot = options.Snapshot();
 2761        snapshot.Validate();
 62
 2263        this.options = snapshot;
 2264        this.timeProvider = timeProvider;
 2265        rsa = CreateRsa(snapshot.KeySizeBits);
 2066        keySizeBits = rsa.KeySize;
 2067    }
 68
 69    /// <inheritdoc />
 70    public ValueTask<SigningResult> SignAsync(
 71        SigningRequest request,
 72        CancellationToken cancellationToken = default)
 73    {
 3374        ArgumentNullException.ThrowIfNull(request);
 3375        cancellationToken.ThrowIfCancellationRequested();
 76
 3377        string? validationFailure = ValidateSigningRequest(request);
 3378        if (validationFailure is not null)
 79        {
 80            // A host that set ReturnUnsignedOnFailure to false asked to be told when signing did not happen. Returning
 81            // unsigned metadata regardless meant a request-validation failure still produced an artifact whose IsSigned
 82            // was false and that no exception announced.
 1983            return options.ReturnUnsignedOnFailure
 1984                ? ValueTask.FromResult(CreateUnsignedFailureResult(request, validationFailure, validationFailure))
 1985                : throw new InvalidOperationException(
 1986                    $"Local-development signing rejected the signing request: {validationFailure}.");
 87        }
 88
 89        try
 1490        {
 91            lock (rsaSync)
 92            {
 1493                ThrowIfDisposed();
 94
 95                // Sign the exact input Core supplied. For requests built by GovernanceArtifactSigner this is the versio
 96                // input that binds the signing policy context; for requests built without one it is the hash text.
 1497                byte[] signature = rsa.SignData(request.SignatureInput.Span, HashAlgorithmName.SHA256, RSASignaturePaddi
 98
 1499                Dictionary<string, string> metadata = CreateBaseMetadata(request);
 14100                metadata["signing_status"] = "signed";
 101
 14102                var signingMetadata = SigningMetadata.Create(
 14103                    signingHash: request.SigningHash,
 14104                    hashAlgorithm: NormalizeHashAlgorithm(request.HashAlgorithm),
 14105                    signature: Convert.ToBase64String(signature),
 14106                    signatureAlgorithm: NormalizeRequired(options.SignatureAlgorithm, LocalDevelopmentSigningOptions.Def
 14107                    keyId: NormalizeRequired(options.KeyId, LocalDevelopmentSigningOptions.DefaultKeyId),
 14108                    keyVersion: NormalizeRequired(options.KeyVersion, LocalDevelopmentSigningOptions.DefaultKeyVersion),
 14109                    provider: NormalizeRequired(options.ProviderName, LocalDevelopmentSigningOptions.DefaultProviderName
 14110                    signedUtc: timeProvider.GetUtcNow(),
 14111                    metadata: metadata);
 112
 14113                return ValueTask.FromResult(SigningResult.FromMetadata(signingMetadata));
 114            }
 115        }
 0116        catch (Exception exception) when (exception is CryptographicException or ObjectDisposedException or InvalidOpera
 117        {
 0118            if (!options.ReturnUnsignedOnFailure)
 119            {
 0120                throw;
 121            }
 122
 0123            return ValueTask.FromResult(CreateUnsignedFailureResult(request, "localdev.signing.failed", exception.GetTyp
 124        }
 14125    }
 126
 127    /// <inheritdoc />
 128    public ValueTask<SignatureVerificationResult> VerifyAsync(
 129        SignatureVerificationRequest request,
 130        CancellationToken cancellationToken = default)
 131    {
 8132        ArgumentNullException.ThrowIfNull(request);
 8133        cancellationToken.ThrowIfCancellationRequested();
 134
 8135        SigningMetadata metadata = request.SigningMetadata;
 136
 8137        if (!metadata.HasSignature)
 138        {
 0139            return ValueTask.FromResult(SignatureVerificationResult.MissingSignature("The local-development verifier rec
 140        }
 141
 8142        if (!FixedTimeHashEquals(request.SigningHash, metadata.SigningHash))
 143        {
 1144            return ValueTask.FromResult(SignatureVerificationResult.Failed(
 1145                "localdev.signature.hash-mismatch",
 1146                SignatureVerificationCategory.HashMismatch,
 1147                "The verification hash does not match the hash recorded in signing metadata."));
 148        }
 149
 7150        if (!IsSupportedHashAlgorithm(metadata.HashAlgorithm))
 151        {
 0152            return ValueTask.FromResult(SignatureVerificationResult.Failed(
 0153                "localdev.signature.hash-algorithm-unsupported",
 0154                SignatureVerificationCategory.UnsupportedAlgorithm,
 0155                "The local-development verifier supports SHA-256 signing metadata only."));
 156        }
 157
 7158        if (!string.Equals(metadata.SignatureAlgorithm, NormalizeRequired(options.SignatureAlgorithm, LocalDevelopmentSi
 159        {
 0160            return ValueTask.FromResult(SignatureVerificationResult.Failed(
 0161                "localdev.signature.algorithm-mismatch",
 0162                SignatureVerificationCategory.UnsupportedAlgorithm,
 0163                "The signature algorithm does not match the configured local-development provider."));
 164        }
 165
 7166        if (!string.Equals(metadata.KeyId, NormalizeRequired(options.KeyId, LocalDevelopmentSigningOptions.DefaultKeyId)
 7167            || !string.Equals(metadata.KeyVersion, NormalizeRequired(options.KeyVersion, LocalDevelopmentSigningOptions.
 168        {
 0169            return ValueTask.FromResult(SignatureVerificationResult.Failed(
 0170                "localdev.signature.key-mismatch",
 0171                SignatureVerificationCategory.UnknownKeyVersion,
 0172                "The signature key reference does not match the configured local-development provider."));
 173        }
 174
 175        // The provider label is not part of the signature input, so it is authenticated only by this verifier refusing 
 176        // label it does not own. Accepting any label let an artifact signed here claim another provider's identity.
 7177        if (!string.Equals(metadata.Provider, NormalizeRequired(options.ProviderName, LocalDevelopmentSigningOptions.Def
 178        {
 1179            return ValueTask.FromResult(SignatureVerificationResult.Failed(
 1180                "localdev.signature.provider-not-trusted",
 1181                SignatureVerificationCategory.UntrustedSigningContext,
 1182                "The signing provider does not match the configured local-development provider."));
 183        }
 184
 185        try
 6186        {
 187            lock (rsaSync)
 188            {
 6189                ThrowIfDisposed();
 190
 6191                byte[] signature = Convert.FromBase64String(metadata.Signature!);
 6192                bool verified = rsa.VerifyData(request.SignatureInput.Span, signature, HashAlgorithmName.SHA256, RSASign
 193
 6194                return ValueTask.FromResult(verified
 6195                    ? SignatureVerificationResult.Verified()
 6196                    : SignatureVerificationResult.Failed(
 6197                        "localdev.signature.invalid",
 6198                        SignatureVerificationCategory.InvalidSignature,
 6199                        "The local-development signature did not verify."));
 200            }
 201        }
 0202        catch (FormatException)
 203        {
 0204            return ValueTask.FromResult(SignatureVerificationResult.Failed(
 0205                "localdev.signature.malformed",
 0206                SignatureVerificationCategory.InvalidSignature,
 0207                "The signature value is not valid Base64."));
 208        }
 0209        catch (Exception exception) when (exception is CryptographicException or ObjectDisposedException or InvalidOpera
 210        {
 0211            return ValueTask.FromResult(SignatureVerificationResult.Failed(
 0212                "localdev.verification.failed",
 0213                SignatureVerificationCategory.Failed,
 0214                exception.GetType().Name));
 215        }
 6216    }
 217
 218    /// <inheritdoc />
 219    public void Dispose()
 44220    {
 221        lock (rsaSync)
 222        {
 44223            if (disposed)
 224            {
 24225                return;
 226            }
 227
 20228            disposed = true;
 20229            rsa.Dispose();
 20230        }
 44231    }
 232
 233    /// <summary>
 234    /// Creates an RSA key of exactly the requested size.
 235    /// </summary>
 236    /// <remarks>
 237    /// The key size is supplied at creation through <see cref="RSA.Create(int)" /> rather than assigned to
 238    /// <see cref="AsymmetricAlgorithm.KeySize" /> afterwards, whose behavior varies across platform providers. A size t
 239    /// provider rejects is reported as <see cref="InvalidOperationException" /> alongside the other configuration failu
 240    /// and a key whose generated size differs from the request is refused rather than used under a different size.
 241    /// </remarks>
 242    /// <param name="requestedKeySizeBits">The configured RSA key size in bits.</param>
 243    /// <returns>An RSA instance whose key size equals <paramref name="requestedKeySizeBits" />.</returns>
 244    /// <exception cref="InvalidOperationException">Thrown when the platform RSA provider cannot generate the requested 
 245    private static RSA CreateRsa(int requestedKeySizeBits)
 246    {
 247        RSA created;
 248
 249        try
 250        {
 22251            created = RSA.Create(requestedKeySizeBits);
 20252        }
 2253        catch (CryptographicException exception)
 254        {
 2255            throw new InvalidOperationException(
 2256                $"Local-development RSA key size {requestedKeySizeBits} bits is not supported by the platform RSA provid
 2257                exception);
 258        }
 259
 20260        if (created.KeySize != requestedKeySizeBits)
 261        {
 0262            int generatedKeySizeBits = created.KeySize;
 0263            created.Dispose();
 264
 0265            throw new InvalidOperationException(
 0266                $"Local-development RSA key size {requestedKeySizeBits} bits is not supported by the platform RSA provid
 267        }
 268
 20269        return created;
 270    }
 271
 272    private static string NormalizeRequired(string? value, string fallback)
 273    {
 166274        return string.IsNullOrWhiteSpace(value)
 166275            ? fallback
 166276            : value.Trim();
 277    }
 278
 279    private static string NormalizeHashAlgorithm(string? hashAlgorithm)
 280    {
 56281        return string.IsNullOrWhiteSpace(hashAlgorithm)
 56282            ? SupportedHashAlgorithm
 56283            : hashAlgorithm.Trim().Equals("SHA256", StringComparison.OrdinalIgnoreCase)
 56284                ? SupportedHashAlgorithm
 56285                : hashAlgorithm.Trim();
 286    }
 287
 288    private static bool IsSupportedHashAlgorithm(string? hashAlgorithm)
 289    {
 24290        string normalized = NormalizeHashAlgorithm(hashAlgorithm);
 291
 24292        return normalized.Equals(SupportedHashAlgorithm, StringComparison.OrdinalIgnoreCase);
 293    }
 294
 295    private static bool FixedTimeHashEquals(string? expectedHash, string? actualHash)
 296    {
 8297        if (expectedHash is null || actualHash is null)
 298        {
 0299            return expectedHash is null && actualHash is null;
 300        }
 301
 8302        byte[] expectedBytes = SigningEncoding.GetBytes(expectedHash);
 8303        byte[] actualBytes = SigningEncoding.GetBytes(actualHash);
 304
 8305        return CryptographicOperations.FixedTimeEquals(expectedBytes, actualBytes);
 306    }
 307
 308    private string? ValidateSigningRequest(SigningRequest request)
 309    {
 33310        if (Volatile.Read(ref disposed))
 311        {
 16312            return "localdev.signing.disposed";
 313        }
 314
 17315        if (!IsSupportedHashAlgorithm(request.HashAlgorithm))
 316        {
 3317            return "localdev.signing.hash-algorithm-unsupported";
 318        }
 319
 14320        string configuredKeyId = NormalizeRequired(options.KeyId, LocalDevelopmentSigningOptions.DefaultKeyId);
 14321        if (request.KeyId is not null && !string.Equals(request.KeyId, configuredKeyId, StringComparison.Ordinal))
 322        {
 0323            return "localdev.signing.key-mismatch";
 324        }
 325
 14326        string configuredKeyVersion = NormalizeRequired(options.KeyVersion, LocalDevelopmentSigningOptions.DefaultKeyVer
 14327        return request.KeyVersion is not null && !string.Equals(request.KeyVersion, configuredKeyVersion, StringComparis
 14328            ? "localdev.signing.key-version-mismatch"
 14329            : null;
 330    }
 331
 332    private SigningResult CreateUnsignedFailureResult(SigningRequest request, string failureCode, string failureMessage)
 333    {
 18334        Dictionary<string, string> metadata = CreateBaseMetadata(request);
 18335        metadata["signing_status"] = "failed";
 18336        metadata["failure_code"] = failureCode;
 18337        metadata["failure_message"] = failureMessage;
 338
 18339        var signingMetadata = SigningMetadata.Create(
 18340            signingHash: request.SigningHash,
 18341            hashAlgorithm: NormalizeHashAlgorithm(request.HashAlgorithm),
 18342            keyId: NormalizeRequired(options.KeyId, LocalDevelopmentSigningOptions.DefaultKeyId),
 18343            keyVersion: NormalizeRequired(options.KeyVersion, LocalDevelopmentSigningOptions.DefaultKeyVersion),
 18344            provider: NormalizeRequired(options.ProviderName, LocalDevelopmentSigningOptions.DefaultProviderName),
 18345            metadata: metadata);
 346
 18347        return SigningResult.FromMetadata(signingMetadata);
 348    }
 349
 350    private Dictionary<string, string> CreateBaseMetadata(SigningRequest request)
 351    {
 32352        Dictionary<string, string> metadata = new(StringComparer.Ordinal)
 32353        {
 32354            ["provider_kind"] = "local-development",
 32355            ["provider_warning"] = "local-development-only",
 32356            ["key_algorithm"] = "RSA",
 32357            ["key_size_bits"] = keySizeBits.ToString(System.Globalization.CultureInfo.InvariantCulture)
 32358        };
 359
 32360        if (request.Purpose is not null)
 361        {
 8362            metadata["purpose"] = request.Purpose;
 363        }
 364
 98365        foreach (KeyValuePair<string, string> item in request.Metadata)
 366        {
 17367            if (string.IsNullOrWhiteSpace(item.Key))
 368            {
 369                continue;
 370            }
 371
 17372            metadata[item.Key.Trim()] = item.Value?.Trim() ?? string.Empty;
 373        }
 374
 32375        return metadata;
 376    }
 377
 378    private void ThrowIfDisposed()
 379    {
 20380        ObjectDisposedException.ThrowIf(disposed, this);
 20381    }
 382}