up
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled
This commit is contained in:
592
src/Cli/StellaOps.Cli/Services/ForensicVerifier.cs
Normal file
592
src/Cli/StellaOps.Cli/Services/ForensicVerifier.cs
Normal file
@@ -0,0 +1,592 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Cli.Output;
|
||||
using StellaOps.Cli.Services.Models;
|
||||
|
||||
namespace StellaOps.Cli.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Verifier for forensic bundles including checksums, DSSE signatures, and chain-of-custody.
|
||||
/// Per CLI-FORENSICS-54-001.
|
||||
/// </summary>
|
||||
internal sealed class ForensicVerifier : IForensicVerifier
|
||||
{
|
||||
private const string PaePrefix = "DSSEv1";
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web)
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
};
|
||||
|
||||
private readonly ILogger<ForensicVerifier> _logger;
|
||||
|
||||
public ForensicVerifier(ILogger<ForensicVerifier> logger)
|
||||
{
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public async Task<ForensicVerificationResult> VerifyBundleAsync(
|
||||
string bundlePath,
|
||||
ForensicVerificationOptions options,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(bundlePath);
|
||||
ArgumentNullException.ThrowIfNull(options);
|
||||
|
||||
var errors = new List<ForensicVerificationError>();
|
||||
var warnings = new List<string>();
|
||||
var verifiedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
_logger.LogDebug("Verifying forensic bundle at {BundlePath}", bundlePath);
|
||||
|
||||
// Check bundle exists
|
||||
if (!File.Exists(bundlePath) && !Directory.Exists(bundlePath))
|
||||
{
|
||||
errors.Add(new ForensicVerificationError
|
||||
{
|
||||
Code = CliErrorCodes.ForensicBundleNotFound,
|
||||
Message = "Bundle path not found",
|
||||
Detail = bundlePath
|
||||
});
|
||||
|
||||
return new ForensicVerificationResult
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
IsValid = false,
|
||||
VerifiedAt = verifiedAt,
|
||||
Errors = errors
|
||||
};
|
||||
}
|
||||
|
||||
// Load manifest
|
||||
var manifestPath = ResolveManifestPath(bundlePath);
|
||||
if (manifestPath is null || !File.Exists(manifestPath))
|
||||
{
|
||||
errors.Add(new ForensicVerificationError
|
||||
{
|
||||
Code = CliErrorCodes.ForensicBundleInvalid,
|
||||
Message = "Manifest not found in bundle",
|
||||
Detail = "Expected manifest.json in bundle root"
|
||||
});
|
||||
|
||||
return new ForensicVerificationResult
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
IsValid = false,
|
||||
VerifiedAt = verifiedAt,
|
||||
Errors = errors
|
||||
};
|
||||
}
|
||||
|
||||
ForensicSnapshotManifest manifest;
|
||||
try
|
||||
{
|
||||
var manifestJson = await File.ReadAllTextAsync(manifestPath, cancellationToken).ConfigureAwait(false);
|
||||
manifest = JsonSerializer.Deserialize<ForensicSnapshotManifest>(manifestJson, SerializerOptions)
|
||||
?? throw new InvalidDataException("Invalid manifest JSON");
|
||||
}
|
||||
catch (Exception ex) when (ex is JsonException or InvalidDataException)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse manifest at {ManifestPath}", manifestPath);
|
||||
errors.Add(new ForensicVerificationError
|
||||
{
|
||||
Code = CliErrorCodes.ForensicBundleInvalid,
|
||||
Message = "Failed to parse manifest",
|
||||
Detail = ex.Message
|
||||
});
|
||||
|
||||
return new ForensicVerificationResult
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
IsValid = false,
|
||||
VerifiedAt = verifiedAt,
|
||||
Errors = errors
|
||||
};
|
||||
}
|
||||
|
||||
var bundleDir = Path.GetDirectoryName(manifestPath) ?? bundlePath;
|
||||
|
||||
// Verify manifest
|
||||
var manifestVerification = await VerifyManifestAsync(manifest, manifestPath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
if (!manifestVerification.IsValid)
|
||||
{
|
||||
errors.Add(new ForensicVerificationError
|
||||
{
|
||||
Code = CliErrorCodes.ForensicChecksumMismatch,
|
||||
Message = "Manifest digest verification failed",
|
||||
Detail = $"Expected: {manifestVerification.Digest}, Computed: {manifestVerification.ComputedDigest}"
|
||||
});
|
||||
}
|
||||
|
||||
// Verify checksums
|
||||
ForensicChecksumVerification? checksumVerification = null;
|
||||
if (options.VerifyChecksums)
|
||||
{
|
||||
checksumVerification = await VerifyChecksumsAsync(manifest, bundleDir, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
foreach (var failure in checksumVerification.FailedArtifacts)
|
||||
{
|
||||
errors.Add(new ForensicVerificationError
|
||||
{
|
||||
Code = CliErrorCodes.ForensicChecksumMismatch,
|
||||
Message = $"Checksum mismatch for artifact {failure.ArtifactId}",
|
||||
Detail = failure.Reason,
|
||||
ArtifactId = failure.ArtifactId
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Verify signatures
|
||||
ForensicSignatureVerification? signatureVerification = null;
|
||||
if (options.VerifySignatures && manifest.Signature is not null)
|
||||
{
|
||||
var trustRoots = options.TrustRoots.ToList();
|
||||
if (!string.IsNullOrWhiteSpace(options.TrustRootPath))
|
||||
{
|
||||
var loadedRoots = await LoadTrustRootsAsync(options.TrustRootPath, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
trustRoots.AddRange(loadedRoots);
|
||||
}
|
||||
|
||||
if (trustRoots.Count == 0)
|
||||
{
|
||||
warnings.Add("No trust roots configured; signature verification skipped");
|
||||
}
|
||||
else
|
||||
{
|
||||
signatureVerification = VerifySignature(manifest, trustRoots);
|
||||
|
||||
if (!signatureVerification.IsValid)
|
||||
{
|
||||
var untrusted = signatureVerification.Signatures
|
||||
.Where(s => !s.IsTrusted)
|
||||
.Select(s => s.KeyId);
|
||||
|
||||
errors.Add(new ForensicVerificationError
|
||||
{
|
||||
Code = signatureVerification.VerifiedSignatures == 0
|
||||
? CliErrorCodes.ForensicSignatureInvalid
|
||||
: CliErrorCodes.ForensicSignatureUntrusted,
|
||||
Message = "Signature verification failed",
|
||||
Detail = string.Join(", ", signatureVerification.Signatures.Select(s => s.Reason).Where(r => r is not null))
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify chain of custody
|
||||
ForensicChainOfCustodyVerification? chainVerification = null;
|
||||
if (options.VerifyChainOfCustody && manifest.Metadata?.ChainOfCustody is { Count: > 0 })
|
||||
{
|
||||
chainVerification = VerifyChainOfCustody(manifest.Metadata.ChainOfCustody, options.StrictTimeline);
|
||||
|
||||
if (!chainVerification.IsValid)
|
||||
{
|
||||
var errorCode = !chainVerification.TimelineValid
|
||||
? CliErrorCodes.ForensicTimelineInvalid
|
||||
: CliErrorCodes.ForensicChainOfCustodyBroken;
|
||||
|
||||
errors.Add(new ForensicVerificationError
|
||||
{
|
||||
Code = errorCode,
|
||||
Message = "Chain of custody verification failed",
|
||||
Detail = chainVerification.Gaps.Count > 0
|
||||
? $"Found {chainVerification.Gaps.Count} timeline gap(s)"
|
||||
: "Invalid entry signatures"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
var isValid = errors.Count == 0 &&
|
||||
manifestVerification.IsValid &&
|
||||
(checksumVerification?.IsValid ?? true) &&
|
||||
(signatureVerification?.IsValid ?? true) &&
|
||||
(chainVerification?.IsValid ?? true);
|
||||
|
||||
return new ForensicVerificationResult
|
||||
{
|
||||
BundlePath = bundlePath,
|
||||
IsValid = isValid,
|
||||
VerifiedAt = verifiedAt,
|
||||
ManifestVerification = manifestVerification,
|
||||
ChecksumVerification = checksumVerification,
|
||||
SignatureVerification = signatureVerification,
|
||||
ChainOfCustodyVerification = chainVerification,
|
||||
Errors = errors,
|
||||
Warnings = warnings
|
||||
};
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<ForensicTrustRoot>> LoadTrustRootsAsync(
|
||||
string trustRootPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(trustRootPath);
|
||||
|
||||
if (!File.Exists(trustRootPath))
|
||||
{
|
||||
_logger.LogWarning("Trust root file not found: {Path}", trustRootPath);
|
||||
return Array.Empty<ForensicTrustRoot>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = await File.ReadAllTextAsync(trustRootPath, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// Try array format first
|
||||
var roots = JsonSerializer.Deserialize<List<ForensicTrustRoot>>(json, SerializerOptions);
|
||||
if (roots is not null)
|
||||
{
|
||||
return roots;
|
||||
}
|
||||
|
||||
// Try single object
|
||||
var singleRoot = JsonSerializer.Deserialize<ForensicTrustRoot>(json, SerializerOptions);
|
||||
if (singleRoot is not null)
|
||||
{
|
||||
return new[] { singleRoot };
|
||||
}
|
||||
|
||||
return Array.Empty<ForensicTrustRoot>();
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to parse trust roots from {Path}", trustRootPath);
|
||||
return Array.Empty<ForensicTrustRoot>();
|
||||
}
|
||||
}
|
||||
|
||||
private static string? ResolveManifestPath(string bundlePath)
|
||||
{
|
||||
if (File.Exists(bundlePath))
|
||||
{
|
||||
// If bundlePath is a file, check if it's the manifest
|
||||
var fileName = Path.GetFileName(bundlePath);
|
||||
if (fileName.Equals("manifest.json", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return bundlePath;
|
||||
}
|
||||
|
||||
// Otherwise look for manifest in same directory
|
||||
var dir = Path.GetDirectoryName(bundlePath);
|
||||
if (dir is not null)
|
||||
{
|
||||
var manifestInDir = Path.Combine(dir, "manifest.json");
|
||||
if (File.Exists(manifestInDir))
|
||||
{
|
||||
return manifestInDir;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Directory.Exists(bundlePath))
|
||||
{
|
||||
var manifestPath = Path.Combine(bundlePath, "manifest.json");
|
||||
return File.Exists(manifestPath) ? manifestPath : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task<ForensicManifestVerification> VerifyManifestAsync(
|
||||
ForensicSnapshotManifest manifest,
|
||||
string manifestPath,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var manifestBytes = await File.ReadAllBytesAsync(manifestPath, cancellationToken).ConfigureAwait(false);
|
||||
var computedDigest = ComputeDigest(manifestBytes, manifest.DigestAlgorithm);
|
||||
|
||||
var isValid = string.Equals(manifest.Digest, computedDigest, StringComparison.OrdinalIgnoreCase) ||
|
||||
string.IsNullOrEmpty(manifest.Digest); // Allow empty digest for unsigned manifests
|
||||
|
||||
return new ForensicManifestVerification
|
||||
{
|
||||
IsValid = isValid,
|
||||
ManifestId = manifest.ManifestId,
|
||||
Version = manifest.Version,
|
||||
Digest = manifest.Digest,
|
||||
DigestAlgorithm = manifest.DigestAlgorithm,
|
||||
ComputedDigest = computedDigest,
|
||||
ArtifactCount = manifest.Artifacts.Count
|
||||
};
|
||||
}
|
||||
|
||||
private async Task<ForensicChecksumVerification> VerifyChecksumsAsync(
|
||||
ForensicSnapshotManifest manifest,
|
||||
string bundleDir,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var failures = new List<ForensicArtifactChecksumFailure>();
|
||||
var verified = 0;
|
||||
|
||||
foreach (var artifact in manifest.Artifacts)
|
||||
{
|
||||
var artifactPath = Path.Combine(bundleDir, artifact.Path);
|
||||
|
||||
if (!File.Exists(artifactPath))
|
||||
{
|
||||
failures.Add(new ForensicArtifactChecksumFailure
|
||||
{
|
||||
ArtifactId = artifact.ArtifactId,
|
||||
Path = artifact.Path,
|
||||
ExpectedDigest = artifact.Digest,
|
||||
ActualDigest = string.Empty,
|
||||
Reason = "Artifact file not found"
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var fileBytes = await File.ReadAllBytesAsync(artifactPath, cancellationToken).ConfigureAwait(false);
|
||||
var actualDigest = ComputeDigest(fileBytes, artifact.DigestAlgorithm);
|
||||
|
||||
if (!string.Equals(artifact.Digest, actualDigest, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
failures.Add(new ForensicArtifactChecksumFailure
|
||||
{
|
||||
ArtifactId = artifact.ArtifactId,
|
||||
Path = artifact.Path,
|
||||
ExpectedDigest = artifact.Digest,
|
||||
ActualDigest = actualDigest,
|
||||
Reason = "Digest mismatch"
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
verified++;
|
||||
}
|
||||
}
|
||||
catch (IOException ex)
|
||||
{
|
||||
failures.Add(new ForensicArtifactChecksumFailure
|
||||
{
|
||||
ArtifactId = artifact.ArtifactId,
|
||||
Path = artifact.Path,
|
||||
ExpectedDigest = artifact.Digest,
|
||||
ActualDigest = string.Empty,
|
||||
Reason = $"IO error: {ex.Message}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return new ForensicChecksumVerification
|
||||
{
|
||||
IsValid = failures.Count == 0,
|
||||
TotalArtifacts = manifest.Artifacts.Count,
|
||||
VerifiedArtifacts = verified,
|
||||
FailedArtifacts = failures
|
||||
};
|
||||
}
|
||||
|
||||
private ForensicSignatureVerification VerifySignature(
|
||||
ForensicSnapshotManifest manifest,
|
||||
IReadOnlyList<ForensicTrustRoot> trustRoots)
|
||||
{
|
||||
if (manifest.Signature is null)
|
||||
{
|
||||
return new ForensicSignatureVerification
|
||||
{
|
||||
IsValid = false,
|
||||
SignatureCount = 0,
|
||||
VerifiedSignatures = 0,
|
||||
Signatures = Array.Empty<ForensicSignatureDetail>()
|
||||
};
|
||||
}
|
||||
|
||||
var signatures = new List<ForensicSignatureDetail>();
|
||||
var verifiedCount = 0;
|
||||
|
||||
// Find matching trust root
|
||||
var matchingRoot = trustRoots.FirstOrDefault(tr =>
|
||||
string.Equals(tr.KeyId, manifest.Signature.KeyId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (matchingRoot is null)
|
||||
{
|
||||
signatures.Add(new ForensicSignatureDetail
|
||||
{
|
||||
KeyId = manifest.Signature.KeyId ?? "unknown",
|
||||
Algorithm = manifest.Signature.Algorithm,
|
||||
IsValid = false,
|
||||
IsTrusted = false,
|
||||
SignedAt = manifest.Signature.SignedAt,
|
||||
Reason = "No matching trust root found"
|
||||
});
|
||||
|
||||
return new ForensicSignatureVerification
|
||||
{
|
||||
IsValid = false,
|
||||
SignatureCount = 1,
|
||||
VerifiedSignatures = 0,
|
||||
Signatures = signatures
|
||||
};
|
||||
}
|
||||
|
||||
// Verify signature
|
||||
var isValid = VerifyRsaPssSignature(
|
||||
manifest.Digest,
|
||||
manifest.Signature.Value,
|
||||
matchingRoot.PublicKey);
|
||||
|
||||
// Check time validity
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var timeValid = (!matchingRoot.NotBefore.HasValue || now >= matchingRoot.NotBefore.Value) &&
|
||||
(!matchingRoot.NotAfter.HasValue || now <= matchingRoot.NotAfter.Value);
|
||||
|
||||
if (isValid && timeValid)
|
||||
{
|
||||
verifiedCount++;
|
||||
}
|
||||
|
||||
signatures.Add(new ForensicSignatureDetail
|
||||
{
|
||||
KeyId = manifest.Signature.KeyId ?? "unknown",
|
||||
Algorithm = manifest.Signature.Algorithm,
|
||||
IsValid = isValid,
|
||||
IsTrusted = isValid && timeValid,
|
||||
SignedAt = manifest.Signature.SignedAt,
|
||||
Fingerprint = matchingRoot.Fingerprint,
|
||||
Reason = !isValid ? "Signature verification failed" :
|
||||
!timeValid ? "Key outside validity period" : null
|
||||
});
|
||||
|
||||
return new ForensicSignatureVerification
|
||||
{
|
||||
IsValid = verifiedCount > 0,
|
||||
SignatureCount = 1,
|
||||
VerifiedSignatures = verifiedCount,
|
||||
Signatures = signatures
|
||||
};
|
||||
}
|
||||
|
||||
private ForensicChainOfCustodyVerification VerifyChainOfCustody(
|
||||
IReadOnlyList<ForensicChainOfCustodyEntry> entries,
|
||||
bool strictTimeline)
|
||||
{
|
||||
var entryVerifications = new List<ForensicChainOfCustodyEntryVerification>();
|
||||
var gaps = new List<ForensicTimelineGap>();
|
||||
var timelineValid = true;
|
||||
var signaturesValid = true;
|
||||
|
||||
DateTimeOffset? lastTimestamp = null;
|
||||
var index = 0;
|
||||
|
||||
foreach (var entry in entries.OrderBy(e => e.Timestamp))
|
||||
{
|
||||
// Check timeline progression
|
||||
if (lastTimestamp.HasValue && entry.Timestamp < lastTimestamp.Value)
|
||||
{
|
||||
timelineValid = false;
|
||||
gaps.Add(new ForensicTimelineGap
|
||||
{
|
||||
FromIndex = index - 1,
|
||||
ToIndex = index,
|
||||
FromTimestamp = lastTimestamp.Value,
|
||||
ToTimestamp = entry.Timestamp,
|
||||
GapDuration = lastTimestamp.Value - entry.Timestamp,
|
||||
Description = "Timestamp out of order"
|
||||
});
|
||||
}
|
||||
else if (strictTimeline && lastTimestamp.HasValue)
|
||||
{
|
||||
var gap = entry.Timestamp - lastTimestamp.Value;
|
||||
if (gap > TimeSpan.FromDays(1))
|
||||
{
|
||||
gaps.Add(new ForensicTimelineGap
|
||||
{
|
||||
FromIndex = index - 1,
|
||||
ToIndex = index,
|
||||
FromTimestamp = lastTimestamp.Value,
|
||||
ToTimestamp = entry.Timestamp,
|
||||
GapDuration = gap,
|
||||
Description = $"Large gap of {gap.TotalHours:F1} hours"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Signature verification (if present)
|
||||
bool? signatureValid = null;
|
||||
if (!string.IsNullOrWhiteSpace(entry.Signature))
|
||||
{
|
||||
// For now, just check signature is present
|
||||
// Full verification would require the signing key
|
||||
signatureValid = true;
|
||||
}
|
||||
|
||||
entryVerifications.Add(new ForensicChainOfCustodyEntryVerification
|
||||
{
|
||||
Index = index,
|
||||
Action = entry.Action,
|
||||
Actor = entry.Actor,
|
||||
Timestamp = entry.Timestamp,
|
||||
SignatureValid = signatureValid,
|
||||
Notes = entry.Notes
|
||||
});
|
||||
|
||||
lastTimestamp = entry.Timestamp;
|
||||
index++;
|
||||
}
|
||||
|
||||
return new ForensicChainOfCustodyVerification
|
||||
{
|
||||
IsValid = timelineValid && signaturesValid && (gaps.Count == 0 || !strictTimeline),
|
||||
EntryCount = entries.Count,
|
||||
TimelineValid = timelineValid,
|
||||
SignaturesValid = signaturesValid,
|
||||
Entries = entryVerifications,
|
||||
Gaps = gaps
|
||||
};
|
||||
}
|
||||
|
||||
private static string ComputeDigest(byte[] data, string algorithm)
|
||||
{
|
||||
byte[] hash;
|
||||
switch (algorithm.ToLowerInvariant())
|
||||
{
|
||||
case "sha256":
|
||||
hash = SHA256.HashData(data);
|
||||
break;
|
||||
case "sha384":
|
||||
hash = SHA384.HashData(data);
|
||||
break;
|
||||
case "sha512":
|
||||
hash = SHA512.HashData(data);
|
||||
break;
|
||||
default:
|
||||
hash = SHA256.HashData(data);
|
||||
break;
|
||||
}
|
||||
|
||||
return Convert.ToHexString(hash).ToLowerInvariant();
|
||||
}
|
||||
|
||||
private static bool VerifyRsaPssSignature(string digest, string signatureBase64, string publicKeyBase64)
|
||||
{
|
||||
try
|
||||
{
|
||||
var publicKeyBytes = Convert.FromBase64String(publicKeyBase64);
|
||||
var signatureBytes = Convert.FromBase64String(signatureBase64);
|
||||
var digestBytes = Convert.FromHexString(digest);
|
||||
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportSubjectPublicKeyInfo(publicKeyBytes, out _);
|
||||
|
||||
return rsa.VerifyHash(digestBytes, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user