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 _logger; public AttestorVerificationEngine( IDsseCanonicalizer canonicalizer, ICryptoHash cryptoHash, IOptions options, ILogger 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 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(); var issuerIssues = new List(); 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() }, new IssuerEvaluationResult { Status = VerificationSectionStatus.Skipped, Mode = issuerFromEntry.Mode ?? "unknown", Issuer = issuerFromEntry.Issuer, SubjectAlternativeName = issuerFromEntry.SubjectAlternativeName, KeyId = issuerFromEntry.KeyId, Issues = Array.Empty() }); } 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 signatureIssues, List issuerIssues) { if (_options.Security.SignerIdentity.KmsKeys.Count == 0) { issuerIssues.Add("kms_key_missing"); return 0; } var signatures = new List(); 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(); 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 signatureIssues, List issuerIssues, DateTimeOffset evaluationTime) { if (bundle.CertificateChain.Count == 0) { issuerIssues.Add("certificate_chain_missing"); return (0, null); } var certificates = new List(); 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() }; } var maxAge = TimeSpan.FromMinutes(maxAgeMinutes.Value); VerificationSectionStatus status; var issues = new List(); 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(); 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(); 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(); 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.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 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 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 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 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 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(); 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(); return false; } private static bool TryDecodeBase64(string value, out byte[] bytes) { try { bytes = Convert.FromBase64String(value); return true; } catch (FormatException) { bytes = Array.Empty(); return false; } } private static bool TryDecodeHex(string value, out byte[] bytes) { try { bytes = Convert.FromHexString(value); return true; } catch (FormatException) { bytes = Array.Empty(); return false; } } private static bool TryDecodeHash(string? value, out byte[] bytes) { bytes = Array.Empty(); 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(); 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; } } }