Add tests and implement StubBearer authentication for Signer endpoints
- Created SignerEndpointsTests to validate the SignDsse and VerifyReferrers endpoints. - Implemented StubBearerAuthenticationDefaults and StubBearerAuthenticationHandler for token-based authentication. - Developed ConcelierExporterClient for managing Trivy DB settings and export operations. - Added TrivyDbSettingsPageComponent for UI interactions with Trivy DB settings, including form handling and export triggering. - Implemented styles and HTML structure for Trivy DB settings page. - Created NotifySmokeCheck tool for validating Redis event streams and Notify deliveries.
This commit is contained in:
		@@ -1448,17 +1448,415 @@ internal static class CommandHandlers
 | 
			
		||||
        {
 | 
			
		||||
            logger.LogError(ex, "Failed to verify revocation bundle.");
 | 
			
		||||
            Environment.ExitCode = 1;
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            loggerFactory.Dispose();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static bool TryParseDetachedJws(string value, out string encodedHeader, out string encodedSignature)
 | 
			
		||||
    {
 | 
			
		||||
        encodedHeader = string.Empty;
 | 
			
		||||
        encodedSignature = string.Empty;
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            loggerFactory.Dispose();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static async Task HandleOfflineKitPullAsync(
 | 
			
		||||
        IServiceProvider services,
 | 
			
		||||
        string? bundleId,
 | 
			
		||||
        string? destinationDirectory,
 | 
			
		||||
        bool overwrite,
 | 
			
		||||
        bool resume,
 | 
			
		||||
        bool verbose,
 | 
			
		||||
        CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        await using var scope = services.CreateAsyncScope();
 | 
			
		||||
        var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
 | 
			
		||||
        var options = scope.ServiceProvider.GetRequiredService<StellaOpsCliOptions>();
 | 
			
		||||
        var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("offline-kit-pull");
 | 
			
		||||
        var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
 | 
			
		||||
        var previousLevel = verbosity.MinimumLevel;
 | 
			
		||||
        verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
 | 
			
		||||
        using var activity = CliActivitySource.Instance.StartActivity("cli.offline.kit.pull", ActivityKind.Client);
 | 
			
		||||
        activity?.SetTag("stellaops.cli.bundle_id", string.IsNullOrWhiteSpace(bundleId) ? "latest" : bundleId);
 | 
			
		||||
        using var duration = CliMetrics.MeasureCommandDuration("offline kit pull");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var targetDirectory = string.IsNullOrWhiteSpace(destinationDirectory)
 | 
			
		||||
                ? options.Offline?.KitsDirectory ?? Path.Combine(Environment.CurrentDirectory, "offline-kits")
 | 
			
		||||
                : destinationDirectory;
 | 
			
		||||
 | 
			
		||||
            targetDirectory = Path.GetFullPath(targetDirectory);
 | 
			
		||||
            Directory.CreateDirectory(targetDirectory);
 | 
			
		||||
 | 
			
		||||
            var result = await client.DownloadOfflineKitAsync(bundleId, targetDirectory, overwrite, resume, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            logger.LogInformation(
 | 
			
		||||
                "Bundle {BundleId} stored at {Path} (captured {Captured:u}, sha256:{Digest}).",
 | 
			
		||||
                result.Descriptor.BundleId,
 | 
			
		||||
                result.BundlePath,
 | 
			
		||||
                result.Descriptor.CapturedAt,
 | 
			
		||||
                result.Descriptor.BundleSha256);
 | 
			
		||||
 | 
			
		||||
            logger.LogInformation("Manifest saved to {Manifest}.", result.ManifestPath);
 | 
			
		||||
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(result.MetadataPath))
 | 
			
		||||
            {
 | 
			
		||||
                logger.LogDebug("Metadata recorded at {Metadata}.", result.MetadataPath);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (result.BundleSignaturePath is not null)
 | 
			
		||||
            {
 | 
			
		||||
                logger.LogInformation("Bundle signature saved to {Signature}.", result.BundleSignaturePath);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (result.ManifestSignaturePath is not null)
 | 
			
		||||
            {
 | 
			
		||||
                logger.LogInformation("Manifest signature saved to {Signature}.", result.ManifestSignaturePath);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            CliMetrics.RecordOfflineKitDownload(result.Descriptor.Kind ?? "unknown", result.FromCache);
 | 
			
		||||
            activity?.SetTag("stellaops.cli.bundle_cache", result.FromCache);
 | 
			
		||||
            Environment.ExitCode = 0;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.LogError(ex, "Failed to download offline kit bundle.");
 | 
			
		||||
            Environment.ExitCode = 1;
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            verbosity.MinimumLevel = previousLevel;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static async Task HandleOfflineKitImportAsync(
 | 
			
		||||
        IServiceProvider services,
 | 
			
		||||
        string bundlePath,
 | 
			
		||||
        string? manifestPath,
 | 
			
		||||
        string? bundleSignaturePath,
 | 
			
		||||
        string? manifestSignaturePath,
 | 
			
		||||
        bool verbose,
 | 
			
		||||
        CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        await using var scope = services.CreateAsyncScope();
 | 
			
		||||
        var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
 | 
			
		||||
        var options = scope.ServiceProvider.GetRequiredService<StellaOpsCliOptions>();
 | 
			
		||||
        var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("offline-kit-import");
 | 
			
		||||
        var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
 | 
			
		||||
        var previousLevel = verbosity.MinimumLevel;
 | 
			
		||||
        verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
 | 
			
		||||
        using var activity = CliActivitySource.Instance.StartActivity("cli.offline.kit.import", ActivityKind.Client);
 | 
			
		||||
        using var duration = CliMetrics.MeasureCommandDuration("offline kit import");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(bundlePath))
 | 
			
		||||
            {
 | 
			
		||||
                logger.LogError("Bundle path is required.");
 | 
			
		||||
                Environment.ExitCode = 1;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            bundlePath = Path.GetFullPath(bundlePath);
 | 
			
		||||
            if (!File.Exists(bundlePath))
 | 
			
		||||
            {
 | 
			
		||||
                logger.LogError("Bundle file {Path} not found.", bundlePath);
 | 
			
		||||
                Environment.ExitCode = 1;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var metadata = await LoadOfflineKitMetadataAsync(bundlePath, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            if (metadata is not null)
 | 
			
		||||
            {
 | 
			
		||||
                manifestPath ??= metadata.ManifestPath;
 | 
			
		||||
                bundleSignaturePath ??= metadata.BundleSignaturePath;
 | 
			
		||||
                manifestSignaturePath ??= metadata.ManifestSignaturePath;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            manifestPath = NormalizeFilePath(manifestPath);
 | 
			
		||||
            bundleSignaturePath = NormalizeFilePath(bundleSignaturePath);
 | 
			
		||||
            manifestSignaturePath = NormalizeFilePath(manifestSignaturePath);
 | 
			
		||||
 | 
			
		||||
            if (manifestPath is null)
 | 
			
		||||
            {
 | 
			
		||||
                manifestPath = TryInferManifestPath(bundlePath);
 | 
			
		||||
                if (manifestPath is not null)
 | 
			
		||||
                {
 | 
			
		||||
                    logger.LogDebug("Using inferred manifest path {Path}.", manifestPath);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (manifestPath is not null && !File.Exists(manifestPath))
 | 
			
		||||
            {
 | 
			
		||||
                logger.LogError("Manifest file {Path} not found.", manifestPath);
 | 
			
		||||
                Environment.ExitCode = 1;
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (bundleSignaturePath is not null && !File.Exists(bundleSignaturePath))
 | 
			
		||||
            {
 | 
			
		||||
                logger.LogWarning("Bundle signature {Path} not found; skipping.", bundleSignaturePath);
 | 
			
		||||
                bundleSignaturePath = null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (manifestSignaturePath is not null && !File.Exists(manifestSignaturePath))
 | 
			
		||||
            {
 | 
			
		||||
                logger.LogWarning("Manifest signature {Path} not found; skipping.", manifestSignaturePath);
 | 
			
		||||
                manifestSignaturePath = null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (metadata is not null)
 | 
			
		||||
            {
 | 
			
		||||
                var computedBundleDigest = await ComputeSha256Async(bundlePath, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                if (!DigestsEqual(computedBundleDigest, metadata.BundleSha256))
 | 
			
		||||
                {
 | 
			
		||||
                    logger.LogError("Bundle digest mismatch. Expected sha256:{Expected} but computed sha256:{Actual}.", metadata.BundleSha256, computedBundleDigest);
 | 
			
		||||
                    Environment.ExitCode = 1;
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (manifestPath is not null)
 | 
			
		||||
                {
 | 
			
		||||
                    var computedManifestDigest = await ComputeSha256Async(manifestPath, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
                    if (!DigestsEqual(computedManifestDigest, metadata.ManifestSha256))
 | 
			
		||||
                    {
 | 
			
		||||
                        logger.LogError("Manifest digest mismatch. Expected sha256:{Expected} but computed sha256:{Actual}.", metadata.ManifestSha256, computedManifestDigest);
 | 
			
		||||
                        Environment.ExitCode = 1;
 | 
			
		||||
                        return;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var request = new OfflineKitImportRequest(
 | 
			
		||||
                bundlePath,
 | 
			
		||||
                manifestPath,
 | 
			
		||||
                bundleSignaturePath,
 | 
			
		||||
                manifestSignaturePath,
 | 
			
		||||
                metadata?.BundleId,
 | 
			
		||||
                metadata?.BundleSha256,
 | 
			
		||||
                metadata?.BundleSize,
 | 
			
		||||
                metadata?.CapturedAt,
 | 
			
		||||
                metadata?.Channel,
 | 
			
		||||
                metadata?.Kind,
 | 
			
		||||
                metadata?.IsDelta,
 | 
			
		||||
                metadata?.BaseBundleId,
 | 
			
		||||
                metadata?.ManifestSha256,
 | 
			
		||||
                metadata?.ManifestSize);
 | 
			
		||||
 | 
			
		||||
            var result = await client.ImportOfflineKitAsync(request, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
            CliMetrics.RecordOfflineKitImport(result.Status);
 | 
			
		||||
 | 
			
		||||
            logger.LogInformation(
 | 
			
		||||
                "Import {ImportId} submitted at {Submitted:u} with status {Status}.",
 | 
			
		||||
                string.IsNullOrWhiteSpace(result.ImportId) ? "<pending>" : result.ImportId,
 | 
			
		||||
                result.SubmittedAt,
 | 
			
		||||
                string.IsNullOrWhiteSpace(result.Status) ? "queued" : result.Status);
 | 
			
		||||
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(result.Message))
 | 
			
		||||
            {
 | 
			
		||||
                logger.LogInformation(result.Message);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Environment.ExitCode = 0;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.LogError(ex, "Offline kit import failed.");
 | 
			
		||||
            Environment.ExitCode = 1;
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            verbosity.MinimumLevel = previousLevel;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public static async Task HandleOfflineKitStatusAsync(
 | 
			
		||||
        IServiceProvider services,
 | 
			
		||||
        bool asJson,
 | 
			
		||||
        bool verbose,
 | 
			
		||||
        CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        await using var scope = services.CreateAsyncScope();
 | 
			
		||||
        var client = scope.ServiceProvider.GetRequiredService<IBackendOperationsClient>();
 | 
			
		||||
        var logger = scope.ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger("offline-kit-status");
 | 
			
		||||
        var verbosity = scope.ServiceProvider.GetRequiredService<VerbosityState>();
 | 
			
		||||
        var previousLevel = verbosity.MinimumLevel;
 | 
			
		||||
        verbosity.MinimumLevel = verbose ? LogLevel.Debug : LogLevel.Information;
 | 
			
		||||
        using var activity = CliActivitySource.Instance.StartActivity("cli.offline.kit.status", ActivityKind.Client);
 | 
			
		||||
        using var duration = CliMetrics.MeasureCommandDuration("offline kit status");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var status = await client.GetOfflineKitStatusAsync(cancellationToken).ConfigureAwait(false);
 | 
			
		||||
 | 
			
		||||
            if (asJson)
 | 
			
		||||
            {
 | 
			
		||||
                var payload = new
 | 
			
		||||
                {
 | 
			
		||||
                    bundleId = status.BundleId,
 | 
			
		||||
                    channel = status.Channel,
 | 
			
		||||
                    kind = status.Kind,
 | 
			
		||||
                    isDelta = status.IsDelta,
 | 
			
		||||
                    baseBundleId = status.BaseBundleId,
 | 
			
		||||
                    capturedAt = status.CapturedAt,
 | 
			
		||||
                    importedAt = status.ImportedAt,
 | 
			
		||||
                    sha256 = status.BundleSha256,
 | 
			
		||||
                    sizeBytes = status.BundleSize,
 | 
			
		||||
                    components = status.Components.Select(component => new
 | 
			
		||||
                    {
 | 
			
		||||
                        component.Name,
 | 
			
		||||
                        component.Version,
 | 
			
		||||
                        component.Digest,
 | 
			
		||||
                        component.CapturedAt,
 | 
			
		||||
                        component.SizeBytes
 | 
			
		||||
                    })
 | 
			
		||||
                };
 | 
			
		||||
 | 
			
		||||
                var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true });
 | 
			
		||||
                Console.WriteLine(json);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                if (string.IsNullOrWhiteSpace(status.BundleId))
 | 
			
		||||
                {
 | 
			
		||||
                    logger.LogInformation("No offline kit bundle has been imported yet.");
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    logger.LogInformation(
 | 
			
		||||
                        "Current bundle {BundleId} ({Kind}) captured {Captured:u}, imported {Imported:u}, sha256:{Digest}, size {Size}.",
 | 
			
		||||
                        status.BundleId,
 | 
			
		||||
                        status.Kind ?? "unknown",
 | 
			
		||||
                        status.CapturedAt ?? default,
 | 
			
		||||
                        status.ImportedAt ?? default,
 | 
			
		||||
                        status.BundleSha256 ?? "<n/a>",
 | 
			
		||||
                        status.BundleSize.HasValue ? status.BundleSize.Value.ToString("N0", CultureInfo.InvariantCulture) : "<n/a>");
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (status.Components.Count > 0)
 | 
			
		||||
                {
 | 
			
		||||
                    var table = new Table().AddColumns("Component", "Version", "Digest", "Captured", "Size (bytes)");
 | 
			
		||||
                    foreach (var component in status.Components)
 | 
			
		||||
                    {
 | 
			
		||||
                        table.AddRow(
 | 
			
		||||
                            component.Name,
 | 
			
		||||
                            string.IsNullOrWhiteSpace(component.Version) ? "-" : component.Version!,
 | 
			
		||||
                            string.IsNullOrWhiteSpace(component.Digest) ? "-" : $"sha256:{component.Digest}",
 | 
			
		||||
                            component.CapturedAt?.ToString("u", CultureInfo.InvariantCulture) ?? "-",
 | 
			
		||||
                            component.SizeBytes.HasValue ? component.SizeBytes.Value.ToString("N0", CultureInfo.InvariantCulture) : "-");
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    AnsiConsole.Write(table);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Environment.ExitCode = 0;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            logger.LogError(ex, "Failed to read offline kit status.");
 | 
			
		||||
            Environment.ExitCode = 1;
 | 
			
		||||
        }
 | 
			
		||||
        finally
 | 
			
		||||
        {
 | 
			
		||||
            verbosity.MinimumLevel = previousLevel;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static async Task<OfflineKitMetadataDocument?> LoadOfflineKitMetadataAsync(string bundlePath, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        var metadataPath = bundlePath + ".metadata.json";
 | 
			
		||||
        if (!File.Exists(metadataPath))
 | 
			
		||||
        {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await using var stream = File.OpenRead(metadataPath);
 | 
			
		||||
            return await JsonSerializer.DeserializeAsync<OfflineKitMetadataDocument>(stream, cancellationToken: cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static string? NormalizeFilePath(string? path)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(path))
 | 
			
		||||
        {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Path.GetFullPath(path);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static string? TryInferManifestPath(string bundlePath)
 | 
			
		||||
    {
 | 
			
		||||
        var directory = Path.GetDirectoryName(bundlePath);
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(directory))
 | 
			
		||||
        {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var baseName = Path.GetFileName(bundlePath);
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(baseName))
 | 
			
		||||
        {
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        baseName = Path.GetFileNameWithoutExtension(baseName);
 | 
			
		||||
        if (baseName.EndsWith(".tar", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
        {
 | 
			
		||||
            baseName = Path.GetFileNameWithoutExtension(baseName);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var candidates = new[]
 | 
			
		||||
        {
 | 
			
		||||
            Path.Combine(directory, $"offline-manifest-{baseName}.json"),
 | 
			
		||||
            Path.Combine(directory, "offline-manifest.json")
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        foreach (var candidate in candidates)
 | 
			
		||||
        {
 | 
			
		||||
            if (File.Exists(candidate))
 | 
			
		||||
            {
 | 
			
		||||
                return Path.GetFullPath(candidate);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return Directory.EnumerateFiles(directory, "offline-manifest*.json").FirstOrDefault();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static bool DigestsEqual(string computed, string? expected)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(expected))
 | 
			
		||||
        {
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return string.Equals(NormalizeDigest(computed), NormalizeDigest(expected), StringComparison.OrdinalIgnoreCase);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static string NormalizeDigest(string digest)
 | 
			
		||||
    {
 | 
			
		||||
        var value = digest.Trim();
 | 
			
		||||
        if (value.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
 | 
			
		||||
        {
 | 
			
		||||
            value = value.Substring("sha256:".Length);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return value.ToLowerInvariant();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static async Task<string> ComputeSha256Async(string path, CancellationToken cancellationToken)
 | 
			
		||||
    {
 | 
			
		||||
        await using var stream = File.OpenRead(path);
 | 
			
		||||
        var hash = await SHA256.HashDataAsync(stream, cancellationToken).ConfigureAwait(false);
 | 
			
		||||
        return Convert.ToHexString(hash).ToLowerInvariant();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static bool TryParseDetachedJws(string value, out string encodedHeader, out string encodedSignature)
 | 
			
		||||
    {
 | 
			
		||||
        encodedHeader = string.Empty;
 | 
			
		||||
        encodedSignature = string.Empty;
 | 
			
		||||
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(value))
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user