Files
git.stella-ops.org/src/Attestor/StellaOps.Attestor.Verify/AttestorVerificationEngine.cs
2026-01-08 08:54:27 +02:00

1015 lines
36 KiB
C#

using System.Collections.Immutable;
using System.Formats.Asn1;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using StellaOps.Attestor.Core.Options;
using StellaOps.Attestor.Core.Storage;
using StellaOps.Attestor.Core.Submission;
using StellaOps.Attestor.Core.Verification;
using StellaOps.Cryptography;
namespace StellaOps.Attestor.Verify;
public sealed class AttestorVerificationEngine : IAttestorVerificationEngine
{
private readonly IDsseCanonicalizer _canonicalizer;
private readonly ICryptoHash _cryptoHash;
private readonly AttestorOptions _options;
private readonly ILogger<AttestorVerificationEngine> _logger;
public AttestorVerificationEngine(
IDsseCanonicalizer canonicalizer,
ICryptoHash cryptoHash,
IOptions<AttestorOptions> options,
ILogger<AttestorVerificationEngine> logger)
{
_canonicalizer = canonicalizer ?? throw new ArgumentNullException(nameof(canonicalizer));
_cryptoHash = cryptoHash ?? throw new ArgumentNullException(nameof(cryptoHash));
_options = options?.Value ?? throw new ArgumentNullException(nameof(options));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public async Task<VerificationReport> EvaluateAsync(
AttestorEntry entry,
AttestorSubmissionRequest.SubmissionBundle? bundle,
DateTimeOffset evaluationTime,
CancellationToken cancellationToken = default)
{
ArgumentNullException.ThrowIfNull(entry);
var signatureIssuer = await EvaluateSignatureAndIssuerAsync(entry, bundle, evaluationTime, cancellationToken).ConfigureAwait(false);
var freshness = EvaluateFreshness(entry, evaluationTime);
var transparency = EvaluateTransparency(entry);
var policy = EvaluatePolicy(entry, signatureIssuer.Signatures, signatureIssuer.Issuer, freshness, transparency, bundle is not null);
return new VerificationReport(policy, signatureIssuer.Issuer, freshness, signatureIssuer.Signatures, transparency);
}
private async Task<(SignatureEvaluationResult Signatures, IssuerEvaluationResult Issuer)> EvaluateSignatureAndIssuerAsync(
AttestorEntry entry,
AttestorSubmissionRequest.SubmissionBundle? bundle,
DateTimeOffset evaluationTime,
CancellationToken cancellationToken)
{
var signatureIssues = new List<string>();
var issuerIssues = new List<string>();
if (bundle is null)
{
var issuerFromEntry = entry.SignerIdentity;
return (
new SignatureEvaluationResult
{
Status = VerificationSectionStatus.Skipped,
BundleProvided = false,
TotalSignatures = 0,
VerifiedSignatures = 0,
RequiredSignatures = Math.Max(1, _options.Verification.MinimumSignatures),
Issues = Array.Empty<string>()
},
new IssuerEvaluationResult
{
Status = VerificationSectionStatus.Skipped,
Mode = issuerFromEntry.Mode ?? "unknown",
Issuer = issuerFromEntry.Issuer,
SubjectAlternativeName = issuerFromEntry.SubjectAlternativeName,
KeyId = issuerFromEntry.KeyId,
Issues = Array.Empty<string>()
});
}
var canonicalRequest = new AttestorSubmissionRequest
{
Bundle = bundle,
Meta = new AttestorSubmissionRequest.SubmissionMeta
{
Artifact = new AttestorSubmissionRequest.ArtifactInfo
{
Sha256 = entry.Artifact.Sha256,
Kind = entry.Artifact.Kind,
ImageDigest = entry.Artifact.ImageDigest,
SubjectUri = entry.Artifact.SubjectUri
},
BundleSha256 = entry.BundleSha256
}
};
byte[] canonicalBundle;
try
{
canonicalBundle = await _canonicalizer.CanonicalizeAsync(canonicalRequest, cancellationToken).ConfigureAwait(false);
}
catch (Exception ex) when (ex is CryptographicException or FormatException)
{
signatureIssues.Add("bundle_canonicalize_failed");
_logger.LogWarning(ex, "Failed to canonicalize DSSE bundle for {Uuid}", entry.RekorUuid);
var issuerFromEntry = entry.SignerIdentity;
return (
new SignatureEvaluationResult
{
Status = VerificationSectionStatus.Fail,
BundleProvided = true,
TotalSignatures = bundle.Dsse.Signatures.Count,
VerifiedSignatures = 0,
RequiredSignatures = Math.Max(1, _options.Verification.MinimumSignatures),
Issues = signatureIssues.ToArray()
},
new IssuerEvaluationResult
{
Status = VerificationSectionStatus.Warn,
Mode = issuerFromEntry.Mode ?? (bundle.Mode ?? "unknown"),
Issuer = issuerFromEntry.Issuer,
SubjectAlternativeName = issuerFromEntry.SubjectAlternativeName,
KeyId = issuerFromEntry.KeyId,
Issues = new[] { "issuer_verification_skipped" }
});
}
var computedHash = _cryptoHash.ComputeHashHexForPurpose(canonicalBundle, HashPurpose.Attestation);
if (!string.Equals(computedHash, entry.BundleSha256, StringComparison.OrdinalIgnoreCase))
{
signatureIssues.Add("bundle_hash_mismatch");
}
var mode = (entry.SignerIdentity.Mode ?? bundle.Mode ?? "unknown").ToLowerInvariant();
var requiredSignatures = Math.Max(1, _options.Verification.MinimumSignatures);
var totalSignatures = bundle.Dsse.Signatures.Count;
var verifiedSignatures = 0;
string? subjectAlternativeName = null;
if (!TryDecodeBase64(bundle.Dsse.PayloadBase64, out var payloadBytes))
{
signatureIssues.Add("bundle_payload_invalid_base64");
return (
new SignatureEvaluationResult
{
Status = VerificationSectionStatus.Fail,
BundleProvided = true,
TotalSignatures = bundle.Dsse.Signatures.Count,
VerifiedSignatures = 0,
RequiredSignatures = requiredSignatures,
Issues = signatureIssues.ToArray()
},
new IssuerEvaluationResult
{
Status = VerificationSectionStatus.Warn,
Mode = mode,
Issuer = entry.SignerIdentity.Issuer,
SubjectAlternativeName = entry.SignerIdentity.SubjectAlternativeName,
KeyId = entry.SignerIdentity.KeyId,
Issues = issuerIssues.ToArray()
});
}
var preAuth = ComputePreAuthEncoding(bundle.Dsse.PayloadType, payloadBytes);
switch (mode)
{
case "kms":
verifiedSignatures = EvaluateKmsSignature(bundle, preAuth, signatureIssues, issuerIssues);
break;
case "keyless":
var keylessResult = EvaluateKeylessSignature(entry, bundle, preAuth, signatureIssues, issuerIssues, evaluationTime);
verifiedSignatures = keylessResult.VerifiedSignatures;
subjectAlternativeName = keylessResult.SubjectAlternativeName;
break;
default:
issuerIssues.Add(string.IsNullOrWhiteSpace(mode) ? "signer_mode_unknown" : $"signer_mode_unsupported:{mode}");
break;
}
var signatureStatus = DetermineSignatureStatus(signatureIssues, verifiedSignatures, requiredSignatures, totalSignatures);
var issuerStatus = DetermineIssuerStatus(issuerIssues, mode, verifiedSignatures > 0);
return (
new SignatureEvaluationResult
{
Status = signatureStatus,
BundleProvided = true,
TotalSignatures = totalSignatures,
VerifiedSignatures = verifiedSignatures,
RequiredSignatures = requiredSignatures,
Issues = signatureIssues.ToArray()
},
new IssuerEvaluationResult
{
Status = issuerStatus,
Mode = mode,
Issuer = entry.SignerIdentity.Issuer,
SubjectAlternativeName = subjectAlternativeName ?? entry.SignerIdentity.SubjectAlternativeName,
KeyId = entry.SignerIdentity.KeyId,
Issues = issuerIssues.ToArray()
});
}
private int EvaluateKmsSignature(
AttestorSubmissionRequest.SubmissionBundle bundle,
byte[] preAuthEncoding,
List<string> signatureIssues,
List<string> issuerIssues)
{
if (_options.Security.SignerIdentity.KmsKeys.Count == 0)
{
issuerIssues.Add("kms_key_missing");
return 0;
}
var signatures = new List<byte[]>();
foreach (var signature in bundle.Dsse.Signatures)
{
if (!TryDecodeBase64(signature.Signature, out var signatureBytes))
{
signatureIssues.Add("signature_invalid_base64");
return 0;
}
signatures.Add(signatureBytes);
}
var expectedSignatures = new List<byte[]>();
foreach (var secret in _options.Security.SignerIdentity.KmsKeys)
{
if (!TryDecodeSecret(secret, out var secretBytes))
{
continue;
}
using var hmac = new HMACSHA256(secretBytes);
expectedSignatures.Add(hmac.ComputeHash(preAuthEncoding));
}
var verified = 0;
foreach (var candidate in signatures)
{
if (expectedSignatures.Any(expected => CryptographicOperations.FixedTimeEquals(expected, candidate)))
{
verified++;
}
}
if (verified == 0)
{
signatureIssues.Add("signature_invalid");
}
return verified;
}
private (int VerifiedSignatures, string? SubjectAlternativeName) EvaluateKeylessSignature(
AttestorEntry entry,
AttestorSubmissionRequest.SubmissionBundle bundle,
byte[] preAuthEncoding,
List<string> signatureIssues,
List<string> issuerIssues,
DateTimeOffset evaluationTime)
{
if (bundle.CertificateChain.Count == 0)
{
issuerIssues.Add("certificate_chain_missing");
return (0, null);
}
var certificates = new List<X509Certificate2>();
try
{
foreach (var pem in bundle.CertificateChain)
{
certificates.Add(X509Certificate2.CreateFromPem(pem));
}
}
catch (Exception ex) when (ex is CryptographicException or ArgumentException)
{
issuerIssues.Add("certificate_chain_invalid");
_logger.LogWarning(ex, "Failed to parse certificate chain for {Uuid}", entry.RekorUuid);
return (0, null);
}
var leafCertificate = certificates[0];
var subjectAltName = GetSubjectAlternativeNames(leafCertificate).FirstOrDefault();
if (_options.Security.SignerIdentity.FulcioRoots.Count > 0)
{
using var chain = new X509Chain
{
ChainPolicy =
{
RevocationMode = X509RevocationMode.NoCheck,
VerificationFlags = X509VerificationFlags.NoFlag,
TrustMode = X509ChainTrustMode.CustomRootTrust,
VerificationTime = evaluationTime.UtcDateTime
}
};
foreach (var rootPath in _options.Security.SignerIdentity.FulcioRoots)
{
try
{
if (File.Exists(rootPath))
{
var rootCertificate = X509CertificateLoader.LoadCertificateFromFile(rootPath);
chain.ChainPolicy.CustomTrustStore.Add(rootCertificate);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to load Fulcio root {Root}", rootPath);
}
}
for (var i = 1; i < certificates.Count; i++)
{
chain.ChainPolicy.ExtraStore.Add(certificates[i]);
}
if (!chain.Build(leafCertificate))
{
var status = string.Join(";", chain.ChainStatus.Select(s => s.StatusInformation.Trim())).Trim(';');
issuerIssues.Add(string.IsNullOrEmpty(status) ? "certificate_chain_untrusted" : $"certificate_chain_untrusted:{status}");
}
}
if (_options.Security.SignerIdentity.AllowedSans.Count > 0)
{
var sans = GetSubjectAlternativeNames(leafCertificate);
if (!sans.Any(san => _options.Security.SignerIdentity.AllowedSans.Contains(san, StringComparer.OrdinalIgnoreCase)))
{
issuerIssues.Add("certificate_san_untrusted");
}
}
var verified = 0;
foreach (var signature in bundle.Dsse.Signatures)
{
if (!TryDecodeBase64(signature.Signature, out var signatureBytes))
{
signatureIssues.Add("signature_invalid_base64");
return (0, subjectAltName);
}
if (TryVerifyWithCertificate(leafCertificate, preAuthEncoding, signatureBytes))
{
verified++;
}
}
if (verified == 0)
{
signatureIssues.Add("signature_invalid");
}
return (verified, subjectAltName);
}
private FreshnessEvaluationResult EvaluateFreshness(AttestorEntry entry, DateTimeOffset evaluationTime)
{
if (entry.CreatedAt == default)
{
return new FreshnessEvaluationResult
{
Status = VerificationSectionStatus.Warn,
CreatedAt = entry.CreatedAt,
EvaluatedAt = evaluationTime,
Age = TimeSpan.Zero,
MaxAge = null,
Issues = new[] { "freshness_unknown" }
};
}
var age = evaluationTime - entry.CreatedAt;
var maxAgeMinutes = _options.Verification.FreshnessMaxAgeMinutes;
var warnAgeMinutes = _options.Verification.FreshnessWarnAgeMinutes;
if (maxAgeMinutes is null)
{
return new FreshnessEvaluationResult
{
Status = VerificationSectionStatus.Skipped,
CreatedAt = entry.CreatedAt,
EvaluatedAt = evaluationTime,
Age = age,
MaxAge = null,
Issues = Array.Empty<string>()
};
}
var maxAge = TimeSpan.FromMinutes(maxAgeMinutes.Value);
VerificationSectionStatus status;
var issues = new List<string>();
if (age > maxAge)
{
status = VerificationSectionStatus.Fail;
issues.Add("freshness_stale");
}
else if (warnAgeMinutes is not null && age > TimeSpan.FromMinutes(warnAgeMinutes.Value))
{
status = VerificationSectionStatus.Warn;
issues.Add("freshness_warning");
}
else
{
status = VerificationSectionStatus.Pass;
}
return new FreshnessEvaluationResult
{
Status = status,
CreatedAt = entry.CreatedAt,
EvaluatedAt = evaluationTime,
Age = age,
MaxAge = maxAge,
Issues = issues.ToArray()
};
}
private TransparencyEvaluationResult EvaluateTransparency(AttestorEntry entry)
{
var issues = new List<string>();
TransparencyEvaluationResult Finalize(VerificationSectionStatus finalStatus, bool proofPresent, bool checkpointPresent, bool inclusionPresent)
{
var witness = entry.Witness;
var witnessPresent = witness is not null;
var witnessMatches = false;
var witnessAggregator = witness?.Aggregator;
var witnessStatus = witness?.Status ?? "missing";
if (witness is null)
{
issues.Add("witness_missing");
if (_options.Verification.RequireWitnessEndorsement)
{
finalStatus = VerificationSectionStatus.Fail;
}
else if (finalStatus != VerificationSectionStatus.Fail)
{
finalStatus = VerificationSectionStatus.Warn;
}
}
else
{
var normalizedStatus = string.IsNullOrWhiteSpace(witness.Status) ? "unknown" : witness.Status!;
if (!string.Equals(normalizedStatus, "endorsed", StringComparison.OrdinalIgnoreCase))
{
issues.Add("witness_status_" + normalizedStatus.ToLowerInvariant());
if (_options.Verification.RequireWitnessEndorsement)
{
finalStatus = VerificationSectionStatus.Fail;
}
else if (finalStatus != VerificationSectionStatus.Fail)
{
finalStatus = VerificationSectionStatus.Warn;
}
}
if (!string.IsNullOrWhiteSpace(witness.RootHash) && entry.Proof?.Checkpoint?.RootHash is not null)
{
if (string.Equals(witness.RootHash, entry.Proof.Checkpoint.RootHash, StringComparison.OrdinalIgnoreCase))
{
witnessMatches = true;
}
else
{
issues.Add("witness_root_mismatch");
if (_options.Verification.RequireWitnessEndorsement)
{
finalStatus = VerificationSectionStatus.Fail;
}
else if (finalStatus != VerificationSectionStatus.Fail)
{
finalStatus = VerificationSectionStatus.Warn;
}
}
}
}
return BuildTransparencyResult(finalStatus, issues, proofPresent, checkpointPresent, inclusionPresent, witnessPresent, witnessMatches, witnessAggregator, witnessStatus);
}
if (entry.Proof is null)
{
issues.Add("proof_missing");
var finalStatus = _options.Verification.RequireTransparencyInclusion ? VerificationSectionStatus.Fail : VerificationSectionStatus.Warn;
return Finalize(finalStatus, false, false, false);
}
if (!TryDecodeHash(entry.BundleSha256, out var bundleHash))
{
issues.Add("bundle_hash_decode_failed");
return Finalize(VerificationSectionStatus.Fail, true, entry.Proof.Checkpoint is not null, entry.Proof.Inclusion is not null);
}
if (entry.Proof.Inclusion is null)
{
issues.Add("proof_inclusion_missing");
var finalStatus = _options.Verification.RequireTransparencyInclusion ? VerificationSectionStatus.Fail : VerificationSectionStatus.Warn;
return Finalize(finalStatus, true, entry.Proof.Checkpoint is not null, false);
}
if (entry.Proof.Inclusion.LeafHash is not null)
{
if (!TryDecodeHash(entry.Proof.Inclusion.LeafHash, out var proofLeaf))
{
issues.Add("proof_leafhash_decode_failed");
return Finalize(VerificationSectionStatus.Fail, true, entry.Proof.Checkpoint is not null, true);
}
if (!CryptographicOperations.FixedTimeEquals(bundleHash, proofLeaf))
{
issues.Add("proof_leafhash_mismatch");
}
}
var current = bundleHash;
var inclusionNodesPresent = entry.Proof.Inclusion.Path.Count > 0;
if (inclusionNodesPresent)
{
var nodes = new List<ProofPathNode>();
foreach (var element in entry.Proof.Inclusion.Path)
{
if (!ProofPathNode.TryParse(element, out var node))
{
issues.Add("proof_path_decode_failed");
return Finalize(VerificationSectionStatus.Fail, true, entry.Proof.Checkpoint is not null, true);
}
if (!node.HasOrientation)
{
issues.Add("proof_path_orientation_missing");
return Finalize(VerificationSectionStatus.Fail, true, entry.Proof.Checkpoint is not null, true);
}
nodes.Add(node);
}
foreach (var node in nodes)
{
current = node.Left ? HashInternal(node.Hash, current) : HashInternal(current, node.Hash);
}
}
if (entry.Proof.Checkpoint is null)
{
issues.Add("checkpoint_missing");
var finalStatus = _options.Verification.RequireCheckpoint ? VerificationSectionStatus.Fail : VerificationSectionStatus.Warn;
return Finalize(finalStatus, true, false, inclusionNodesPresent);
}
if (!TryDecodeHash(entry.Proof.Checkpoint.RootHash, out var rootHash))
{
issues.Add("checkpoint_root_decode_failed");
return Finalize(VerificationSectionStatus.Fail, true, true, inclusionNodesPresent);
}
if (!CryptographicOperations.FixedTimeEquals(current, rootHash))
{
issues.Add("proof_root_mismatch");
}
var status = issues.Count == 0 ? VerificationSectionStatus.Pass : VerificationSectionStatus.Fail;
return Finalize(status, true, true, inclusionNodesPresent);
}
private PolicyEvaluationResult EvaluatePolicy(
AttestorEntry entry,
SignatureEvaluationResult signatures,
IssuerEvaluationResult issuer,
FreshnessEvaluationResult freshness,
TransparencyEvaluationResult transparency,
bool bundleProvided)
{
var issues = new List<string>();
var status = VerificationSectionStatus.Pass;
if (!string.Equals(entry.Status, "included", StringComparison.OrdinalIgnoreCase))
{
issues.Add($"log_status_{entry.Status.ToLowerInvariant()}");
status = VerificationSectionStatus.Fail;
}
if (_options.Verification.RequireBundleForSignatureValidation && !bundleProvided)
{
issues.Add("bundle_required");
status = VerificationSectionStatus.Fail;
}
status = CombinePolicyStatus(status, signatures.Status, "signatures", issues);
status = CombinePolicyStatus(status, issuer.Status, "issuer", issues);
status = CombinePolicyStatus(status, freshness.Status, "freshness", issues, warnOnly: true);
status = CombinePolicyStatus(status, transparency.Status, "transparency", issues);
var verdict = status switch
{
VerificationSectionStatus.Fail => "fail",
VerificationSectionStatus.Warn => "warn",
VerificationSectionStatus.Pass => "pass",
_ => "unknown"
};
var attributes = ImmutableDictionary<string, string>.Empty
.Add("status", entry.Status ?? "unknown")
.Add("logBackend", entry.Log.Backend ?? "primary")
.Add("logUrl", entry.Log.Url ?? string.Empty);
if (entry.Index.HasValue)
{
attributes = attributes.Add("index", entry.Index.Value.ToString());
}
if (entry.Proof?.Checkpoint?.Timestamp is not null)
{
attributes = attributes.Add("checkpointTs", entry.Proof.Checkpoint.Timestamp.Value.ToString("O", CultureInfo.InvariantCulture));
}
return new PolicyEvaluationResult
{
Status = status,
PolicyId = _options.Verification.PolicyId,
PolicyVersion = _options.Verification.PolicyVersion,
Verdict = verdict,
Issues = issues.Distinct(StringComparer.OrdinalIgnoreCase).ToArray(),
Attributes = attributes
};
}
private static VerificationSectionStatus DetermineSignatureStatus(
IReadOnlyCollection<string> issues,
int verified,
int required,
int total)
{
if (total == 0)
{
return VerificationSectionStatus.Fail;
}
if (issues.Count > 0)
{
return issues.Contains("signature_invalid", StringComparer.OrdinalIgnoreCase)
|| issues.Contains("bundle_payload_invalid_base64", StringComparer.OrdinalIgnoreCase)
|| issues.Contains("bundle_hash_mismatch", StringComparer.OrdinalIgnoreCase)
? VerificationSectionStatus.Fail
: VerificationSectionStatus.Warn;
}
return verified >= required ? VerificationSectionStatus.Pass : VerificationSectionStatus.Fail;
}
private static VerificationSectionStatus DetermineIssuerStatus(
IReadOnlyCollection<string> issues,
string mode,
bool signatureVerified)
{
if (issues.Count == 0)
{
return signatureVerified ? VerificationSectionStatus.Pass : VerificationSectionStatus.Warn;
}
if (issues.Any(issue => issue.StartsWith("certificate_", StringComparison.OrdinalIgnoreCase) || issue.StartsWith("kms_", StringComparison.OrdinalIgnoreCase)))
{
return VerificationSectionStatus.Fail;
}
if (issues.Any(issue => issue.StartsWith("signer_mode", StringComparison.OrdinalIgnoreCase)))
{
return VerificationSectionStatus.Fail;
}
return VerificationSectionStatus.Warn;
}
private static VerificationSectionStatus CombinePolicyStatus(
VerificationSectionStatus current,
VerificationSectionStatus next,
string component,
List<string> issues,
bool warnOnly = false)
{
if (next == VerificationSectionStatus.Fail)
{
issues.Add($"policy_blocked:{component}");
return VerificationSectionStatus.Fail;
}
if (next == VerificationSectionStatus.Warn && !warnOnly)
{
issues.Add($"policy_warn:{component}");
return current == VerificationSectionStatus.Fail ? current : VerificationSectionStatus.Warn;
}
if (next == VerificationSectionStatus.Warn && warnOnly)
{
issues.Add($"policy_warn:{component}");
return current;
}
return current;
}
private static TransparencyEvaluationResult BuildTransparencyResult(
VerificationSectionStatus status,
List<string> issues,
bool proofPresent,
bool checkpointPresent,
bool inclusionPresent,
bool witnessPresent,
bool witnessMatches,
string? witnessAggregator,
string witnessStatus)
{
return new TransparencyEvaluationResult
{
Status = status,
ProofPresent = proofPresent,
CheckpointPresent = checkpointPresent,
InclusionPathPresent = inclusionPresent,
WitnessPresent = witnessPresent,
WitnessMatchesRoot = witnessMatches,
WitnessAggregator = witnessAggregator,
WitnessStatus = witnessStatus,
Issues = issues.ToArray()
};
}
private static bool TryVerifyWithCertificate(X509Certificate2 certificate, byte[] preAuthEncoding, byte[] signature)
{
try
{
var ecdsa = certificate.GetECDsaPublicKey();
if (ecdsa is not null)
{
using (ecdsa)
{
if (ecdsa.VerifyData(preAuthEncoding, signature, HashAlgorithmName.SHA256))
{
return true;
}
}
}
var rsa = certificate.GetRSAPublicKey();
if (rsa is not null)
{
using (rsa)
{
if (rsa.VerifyData(preAuthEncoding, signature, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1))
{
return true;
}
}
}
}
catch (CryptographicException)
{
return false;
}
return false;
}
private static IEnumerable<string> GetSubjectAlternativeNames(X509Certificate2 certificate)
{
foreach (var extension in certificate.Extensions)
{
if (string.Equals(extension.Oid?.Value, "2.5.29.17", StringComparison.Ordinal))
{
AsnReader reader;
try
{
reader = new AsnReader(extension.RawData, AsnEncodingRules.DER);
}
catch (AsnContentException)
{
yield break;
}
var sequence = reader.ReadSequence();
while (sequence.HasData)
{
var tag = sequence.PeekTag();
if (tag.TagClass != TagClass.ContextSpecific)
{
sequence.ReadEncodedValue();
continue;
}
switch (tag.TagValue)
{
case 1:
yield return sequence.ReadCharacterString(UniversalTagNumber.IA5String, new Asn1Tag(TagClass.ContextSpecific, 1));
break;
case 2:
yield return sequence.ReadCharacterString(UniversalTagNumber.IA5String, new Asn1Tag(TagClass.ContextSpecific, 2));
break;
case 6:
yield return sequence.ReadCharacterString(UniversalTagNumber.IA5String, new Asn1Tag(TagClass.ContextSpecific, 6));
break;
case 7:
var ipBytes = sequence.ReadOctetString(new Asn1Tag(TagClass.ContextSpecific, 7));
yield return new IPAddress(ipBytes).ToString();
break;
default:
sequence.ReadEncodedValue();
break;
}
}
}
}
}
private static byte[] ComputePreAuthEncoding(string payloadType, byte[] payload)
{
var payloadTypeValue = payloadType ?? string.Empty;
var payloadTypeBytes = Encoding.UTF8.GetBytes(payloadTypeValue);
var payloadTypeLength = Encoding.ASCII.GetBytes(payloadTypeBytes.Length.ToString(System.Globalization.CultureInfo.InvariantCulture));
var payloadLength = Encoding.ASCII.GetBytes(payload.Length.ToString(System.Globalization.CultureInfo.InvariantCulture));
var space = new byte[] { (byte)' ' };
var totalLength = 6 + space.Length + payloadTypeLength.Length + space.Length + payloadTypeBytes.Length
+ space.Length + payloadLength.Length + space.Length + payload.Length;
var buffer = new byte[totalLength];
var offset = 0;
static void CopyBytes(byte[] source, byte[] destination, ref int index)
{
Buffer.BlockCopy(source, 0, destination, index, source.Length);
index += source.Length;
}
CopyBytes(Encoding.ASCII.GetBytes("DSSEv1"), buffer, ref offset);
CopyBytes(space, buffer, ref offset);
CopyBytes(payloadTypeLength, buffer, ref offset);
CopyBytes(space, buffer, ref offset);
CopyBytes(payloadTypeBytes, buffer, ref offset);
CopyBytes(space, buffer, ref offset);
CopyBytes(payloadLength, buffer, ref offset);
CopyBytes(space, buffer, ref offset);
payload.CopyTo(buffer.AsSpan(offset));
return buffer;
}
private byte[] HashInternal(byte[] left, byte[] right)
{
var buffer = new byte[1 + left.Length + right.Length];
buffer[0] = 0x01;
Buffer.BlockCopy(left, 0, buffer, 1, left.Length);
Buffer.BlockCopy(right, 0, buffer, 1 + left.Length, right.Length);
return _cryptoHash.ComputeHashForPurpose(buffer, HashPurpose.Merkle);
}
private static bool TryDecodeSecret(string value, out byte[] bytes)
{
if (string.IsNullOrWhiteSpace(value))
{
bytes = Array.Empty<byte>();
return false;
}
value = value.Trim();
if (value.StartsWith("base64:", StringComparison.OrdinalIgnoreCase))
{
return TryDecodeBase64(value[7..], out bytes);
}
if (value.StartsWith("hex:", StringComparison.OrdinalIgnoreCase))
{
return TryDecodeHex(value[4..], out bytes);
}
if (TryDecodeBase64(value, out bytes))
{
return true;
}
if (TryDecodeHex(value, out bytes))
{
return true;
}
bytes = Array.Empty<byte>();
return false;
}
private static bool TryDecodeBase64(string value, out byte[] bytes)
{
try
{
bytes = Convert.FromBase64String(value);
return true;
}
catch (FormatException)
{
bytes = Array.Empty<byte>();
return false;
}
}
private static bool TryDecodeHex(string value, out byte[] bytes)
{
try
{
bytes = Convert.FromHexString(value);
return true;
}
catch (FormatException)
{
bytes = Array.Empty<byte>();
return false;
}
}
private static bool TryDecodeHash(string? value, out byte[] bytes)
{
bytes = Array.Empty<byte>();
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
var trimmed = value.Trim();
if (TryDecodeHex(trimmed, out bytes))
{
return true;
}
if (TryDecodeBase64(trimmed, out bytes))
{
return true;
}
bytes = Array.Empty<byte>();
return false;
}
private readonly struct ProofPathNode
{
private ProofPathNode(bool hasOrientation, bool left, byte[] hash)
{
HasOrientation = hasOrientation;
Left = left;
Hash = hash;
}
public bool HasOrientation { get; }
public bool Left { get; }
public byte[] Hash { get; }
public static bool TryParse(string value, out ProofPathNode node)
{
node = default;
if (string.IsNullOrWhiteSpace(value))
{
return false;
}
var trimmed = value.Trim();
var parts = trimmed.Split(':', 2);
bool hasOrientation = false;
bool left = false;
string hashPart = trimmed;
if (parts.Length == 2)
{
var prefix = parts[0].Trim().ToLowerInvariant();
if (prefix is "l" or "left")
{
hasOrientation = true;
left = true;
}
else if (prefix is "r" or "right")
{
hasOrientation = true;
left = false;
}
hashPart = parts[1].Trim();
}
if (!TryDecodeHash(hashPart, out var hash))
{
return false;
}
node = new ProofPathNode(hasOrientation, left, hash);
return true;
}
}
}