consolidation of some of the modules, localization fixes, product advisories work, qa work
This commit is contained in:
@@ -10,12 +10,15 @@ using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using StellaOps.Attestor.Core.Predicates;
|
||||
using StellaOps.Attestor.Core.Signing;
|
||||
using StellaOps.Attestor.Core.Verification;
|
||||
using StellaOps.Attestor.Envelope;
|
||||
using StellaOps.Attestor.Serialization;
|
||||
using StellaOps.Cryptography;
|
||||
using System.CommandLine;
|
||||
using System.Globalization;
|
||||
using System.IO.Compression;
|
||||
using System.Security.Cryptography;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
@@ -405,6 +408,7 @@ public static class BundleVerifyCommand
|
||||
|
||||
var allDsseFiles = rootDsseFiles.Concat(additionalDsseFiles).ToList();
|
||||
var verified = 0;
|
||||
var allPassed = true;
|
||||
|
||||
foreach (var dsseFile in allDsseFiles)
|
||||
{
|
||||
@@ -424,15 +428,55 @@ public static class BundleVerifyCommand
|
||||
if (envelope?.Signatures == null || envelope.Signatures.Count == 0)
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck($"dsse:{dsseFile}", false, "No signatures found"));
|
||||
allPassed = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If trust root provided, verify signature
|
||||
if (!string.IsNullOrEmpty(trustRoot))
|
||||
{
|
||||
// In production, actually verify the signature
|
||||
result.Checks.Add(new VerificationCheck($"dsse:{dsseFile}", true,
|
||||
$"Signature verified ({envelope.Signatures.Count} signature(s))"));
|
||||
if (!File.Exists(trustRoot))
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck($"dsse:{dsseFile}", false,
|
||||
$"Trust root file not found: {trustRoot}"));
|
||||
allPassed = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(envelope.Payload) || string.IsNullOrWhiteSpace(envelope.PayloadType))
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck($"dsse:{dsseFile}", false,
|
||||
"DSSE payload or payloadType missing"));
|
||||
allPassed = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
var signatureVerified = false;
|
||||
string? lastError = null;
|
||||
foreach (var signature in envelope.Signatures)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(signature.Sig))
|
||||
{
|
||||
lastError = "Signature value missing";
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryVerifyDsseSignature(trustRoot, envelope.PayloadType, envelope.Payload, signature.Sig, out var error))
|
||||
{
|
||||
signatureVerified = true;
|
||||
break;
|
||||
}
|
||||
|
||||
lastError = error;
|
||||
}
|
||||
|
||||
result.Checks.Add(new VerificationCheck($"dsse:{dsseFile}", signatureVerified,
|
||||
signatureVerified
|
||||
? $"Cryptographic signature verified ({envelope.Signatures.Count} signature(s))"
|
||||
: $"Signature verification failed: {lastError ?? "invalid_signature"}"));
|
||||
if (!signatureVerified)
|
||||
{
|
||||
allPassed = false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -446,7 +490,97 @@ public static class BundleVerifyCommand
|
||||
verified++;
|
||||
}
|
||||
|
||||
return verified > 0;
|
||||
return verified > 0 && allPassed;
|
||||
}
|
||||
|
||||
private static bool TryVerifyDsseSignature(
|
||||
string trustRootPath,
|
||||
string payloadType,
|
||||
string payloadBase64,
|
||||
string signatureBase64,
|
||||
out string? error)
|
||||
{
|
||||
error = null;
|
||||
try
|
||||
{
|
||||
var payloadBytes = Convert.FromBase64String(payloadBase64);
|
||||
var signatureBytes = Convert.FromBase64String(signatureBase64);
|
||||
var pae = BuildDssePae(payloadType, payloadBytes);
|
||||
var publicKeyPem = File.ReadAllText(trustRootPath);
|
||||
|
||||
try
|
||||
{
|
||||
using var rsa = RSA.Create();
|
||||
rsa.ImportFromPem(publicKeyPem);
|
||||
if (rsa.VerifyData(pae, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Try certificate/ECDSA path below.
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var cert = X509CertificateLoader.LoadCertificateFromFile(trustRootPath);
|
||||
using var certKey = cert.GetRSAPublicKey();
|
||||
if (certKey is not null &&
|
||||
certKey.VerifyData(pae, signatureBytes, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Try ECDSA path.
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var ecdsa = ECDsa.Create();
|
||||
ecdsa.ImportFromPem(publicKeyPem);
|
||||
return ecdsa.VerifyData(pae, signatureBytes, HashAlgorithmName.SHA256);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] BuildDssePae(string payloadType, byte[] payload)
|
||||
{
|
||||
var header = Encoding.UTF8.GetBytes("DSSEv1");
|
||||
var payloadTypeBytes = Encoding.UTF8.GetBytes(payloadType ?? string.Empty);
|
||||
var payloadTypeLengthBytes = Encoding.UTF8.GetBytes(payloadTypeBytes.Length.ToString(CultureInfo.InvariantCulture));
|
||||
var payloadLengthBytes = Encoding.UTF8.GetBytes(payload.Length.ToString(CultureInfo.InvariantCulture));
|
||||
var space = new[] { (byte)' ' };
|
||||
|
||||
var output = new byte[
|
||||
header.Length + space.Length + payloadTypeLengthBytes.Length + space.Length +
|
||||
payloadTypeBytes.Length + space.Length + payloadLengthBytes.Length + space.Length +
|
||||
payload.Length];
|
||||
|
||||
var offset = 0;
|
||||
Buffer.BlockCopy(header, 0, output, offset, header.Length); offset += header.Length;
|
||||
Buffer.BlockCopy(space, 0, output, offset, space.Length); offset += space.Length;
|
||||
Buffer.BlockCopy(payloadTypeLengthBytes, 0, output, offset, payloadTypeLengthBytes.Length); offset += payloadTypeLengthBytes.Length;
|
||||
Buffer.BlockCopy(space, 0, output, offset, space.Length); offset += space.Length;
|
||||
Buffer.BlockCopy(payloadTypeBytes, 0, output, offset, payloadTypeBytes.Length); offset += payloadTypeBytes.Length;
|
||||
Buffer.BlockCopy(space, 0, output, offset, space.Length); offset += space.Length;
|
||||
Buffer.BlockCopy(payloadLengthBytes, 0, output, offset, payloadLengthBytes.Length); offset += payloadLengthBytes.Length;
|
||||
Buffer.BlockCopy(space, 0, output, offset, space.Length); offset += space.Length;
|
||||
Buffer.BlockCopy(payload, 0, output, offset, payload.Length);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static async Task<bool> VerifyRekorProofsAsync(
|
||||
@@ -468,45 +602,483 @@ public static class BundleVerifyCommand
|
||||
}
|
||||
|
||||
var proofJson = await File.ReadAllTextAsync(proofPath, ct);
|
||||
var proof = JsonSerializer.Deserialize<RekorProofDto>(proofJson, JsonOptions);
|
||||
|
||||
if (proof == null)
|
||||
JsonDocument proofDocument;
|
||||
try
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck("rekor:proof", false, "Failed to parse proof"));
|
||||
proofDocument = JsonDocument.Parse(proofJson);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck("rekor:proof", false, $"proof-parse-failed: {ex.Message}"));
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify Merkle proof
|
||||
if (!string.IsNullOrEmpty(checkpointPath))
|
||||
using (proofDocument)
|
||||
{
|
||||
var checkpointJson = await File.ReadAllTextAsync(checkpointPath, ct);
|
||||
var checkpoint = JsonSerializer.Deserialize<CheckpointDto>(checkpointJson, JsonOptions);
|
||||
if (!TryReadLogIndex(proofDocument.RootElement, out var logIndex))
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck("rekor:proof", false, "proof-log-index-missing"));
|
||||
return false;
|
||||
}
|
||||
|
||||
result.Checks.Add(new VerificationCheck("rekor:proof", true, $"Proof parsed (log index: {logIndex})"));
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(checkpointPath))
|
||||
{
|
||||
if (!File.Exists(checkpointPath))
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck(
|
||||
"rekor:inclusion",
|
||||
false,
|
||||
$"checkpoint-not-found: {checkpointPath}"));
|
||||
return false;
|
||||
}
|
||||
|
||||
var checkpointJson = await File.ReadAllTextAsync(checkpointPath, ct);
|
||||
if (!TryParseCheckpoint(checkpointJson, out var checkpoint, out var checkpointError))
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck(
|
||||
"rekor:inclusion",
|
||||
false,
|
||||
$"checkpoint-invalid: {checkpointError ?? "unknown"}"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (logIndex < 0 || logIndex >= checkpoint.TreeSize)
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck(
|
||||
"rekor:inclusion",
|
||||
false,
|
||||
$"proof-log-index-out-of-range: logIndex={logIndex}, checkpointTreeSize={checkpoint.TreeSize}"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryResolveProofRootHash(proofDocument.RootElement, out var proofRootHash, out var rootError))
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck(
|
||||
"rekor:inclusion",
|
||||
false,
|
||||
$"proof-root-hash-invalid: {rootError ?? "missing"}"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CryptographicOperations.FixedTimeEquals(proofRootHash, checkpoint.RootHash))
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck(
|
||||
"rekor:inclusion",
|
||||
false,
|
||||
"proof-root-hash-mismatch-with-checkpoint"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryResolveProofHashes(proofDocument.RootElement, out var proofHashes, out var hashError))
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck(
|
||||
"rekor:inclusion",
|
||||
false,
|
||||
$"proof-hashes-invalid: {hashError ?? "missing"}"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryResolveProofTreeSize(proofDocument.RootElement, checkpoint.TreeSize, out var proofTreeSize))
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck(
|
||||
"rekor:inclusion",
|
||||
false,
|
||||
"proof-tree-size-invalid"));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryResolveLeafHash(proofDocument.RootElement, out var leafHash, out var leafError))
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck(
|
||||
"rekor:inclusion",
|
||||
false,
|
||||
$"proof-leaf-hash-missing: {leafError ?? "cannot-verify-merkle"}"));
|
||||
return false;
|
||||
}
|
||||
|
||||
var inclusionValid = MerkleProofVerifier.VerifyInclusion(
|
||||
leafHash,
|
||||
logIndex,
|
||||
proofTreeSize,
|
||||
proofHashes,
|
||||
checkpoint.RootHash);
|
||||
|
||||
if (!inclusionValid)
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck(
|
||||
"rekor:inclusion",
|
||||
false,
|
||||
"proof-merkle-verification-failed"));
|
||||
return false;
|
||||
}
|
||||
|
||||
result.Checks.Add(new VerificationCheck("rekor:inclusion", true, $"Inclusion verified at log index {logIndex}"));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!offline)
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck("rekor:inclusion", true,
|
||||
$"Log index {logIndex} present - checkpoint not provided for offline verification")
|
||||
{
|
||||
Severity = "warning"
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// In production, verify inclusion proof against checkpoint
|
||||
result.Checks.Add(new VerificationCheck("rekor:inclusion", true,
|
||||
$"Inclusion verified at log index {proof.LogIndex}"));
|
||||
}
|
||||
else if (!offline)
|
||||
{
|
||||
// Online: fetch checkpoint and verify
|
||||
result.Checks.Add(new VerificationCheck("rekor:inclusion", true,
|
||||
$"Log index {proof.LogIndex} present - online verification available")
|
||||
$"Log index {logIndex} present - no checkpoint for offline verification")
|
||||
{
|
||||
Severity = "warning"
|
||||
});
|
||||
return true;
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
private static bool TryParseCheckpoint(
|
||||
string checkpointJson,
|
||||
out ParsedCheckpoint checkpoint,
|
||||
out string? error)
|
||||
{
|
||||
checkpoint = default;
|
||||
error = null;
|
||||
|
||||
JsonDocument document;
|
||||
try
|
||||
{
|
||||
result.Checks.Add(new VerificationCheck("rekor:inclusion", true,
|
||||
$"Log index {proof.LogIndex} present - no checkpoint for offline verification")
|
||||
document = JsonDocument.Parse(checkpointJson);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
error = ex.Message;
|
||||
return false;
|
||||
}
|
||||
|
||||
using (document)
|
||||
{
|
||||
var root = document.RootElement;
|
||||
var checkpointElement = root.TryGetProperty("checkpoint", out var nestedCheckpoint) &&
|
||||
nestedCheckpoint.ValueKind == JsonValueKind.Object
|
||||
? nestedCheckpoint
|
||||
: root;
|
||||
|
||||
if (!TryGetInt64Property(checkpointElement, "treeSize", out var treeSize))
|
||||
{
|
||||
Severity = "warning"
|
||||
});
|
||||
if (!TryGetInt64Property(checkpointElement, "size", out treeSize))
|
||||
{
|
||||
error = "treeSize/size missing";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryGetStringProperty(checkpointElement, "rootHash", out var rootHashString))
|
||||
{
|
||||
if (!TryGetStringProperty(checkpointElement, "hash", out rootHashString))
|
||||
{
|
||||
error = "rootHash/hash missing";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryDecodeHashValue(rootHashString, out var rootHashBytes))
|
||||
{
|
||||
error = "root hash must be lowercase hex, sha256:hex, or base64";
|
||||
return false;
|
||||
}
|
||||
|
||||
checkpoint = new ParsedCheckpoint(treeSize, rootHashBytes);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadLogIndex(JsonElement root, out long logIndex)
|
||||
{
|
||||
if (TryGetInt64Property(root, "logIndex", out logIndex))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryGetObjectProperty(root, "inclusion", out var inclusion) &&
|
||||
TryGetInt64Property(inclusion, "logIndex", out logIndex))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryGetObjectProperty(root, "inclusionProof", out var inclusionProof) &&
|
||||
TryGetInt64Property(inclusionProof, "logIndex", out logIndex))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
logIndex = -1;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolveProofTreeSize(JsonElement root, long fallbackTreeSize, out long treeSize)
|
||||
{
|
||||
if (TryGetInt64Property(root, "treeSize", out treeSize))
|
||||
{
|
||||
return treeSize > 0;
|
||||
}
|
||||
|
||||
if (TryGetObjectProperty(root, "inclusion", out var inclusion) &&
|
||||
TryGetInt64Property(inclusion, "treeSize", out treeSize))
|
||||
{
|
||||
return treeSize > 0;
|
||||
}
|
||||
|
||||
if (TryGetObjectProperty(root, "inclusionProof", out var inclusionProof) &&
|
||||
TryGetInt64Property(inclusionProof, "treeSize", out treeSize))
|
||||
{
|
||||
return treeSize > 0;
|
||||
}
|
||||
|
||||
treeSize = fallbackTreeSize;
|
||||
return treeSize > 0;
|
||||
}
|
||||
|
||||
private static bool TryResolveProofRootHash(JsonElement root, out byte[] rootHash, out string? error)
|
||||
{
|
||||
rootHash = Array.Empty<byte>();
|
||||
error = null;
|
||||
|
||||
string? rootHashString = null;
|
||||
if (TryGetStringProperty(root, "rootHash", out var directRootHash))
|
||||
{
|
||||
rootHashString = directRootHash;
|
||||
}
|
||||
else if (TryGetObjectProperty(root, "inclusion", out var inclusion) &&
|
||||
TryGetStringProperty(inclusion, "rootHash", out var inclusionRootHash))
|
||||
{
|
||||
rootHashString = inclusionRootHash;
|
||||
}
|
||||
else if (TryGetObjectProperty(root, "inclusionProof", out var inclusionProof) &&
|
||||
TryGetStringProperty(inclusionProof, "rootHash", out var inclusionProofRootHash))
|
||||
{
|
||||
rootHashString = inclusionProofRootHash;
|
||||
}
|
||||
else if (TryGetObjectProperty(root, "checkpoint", out var checkpointObject))
|
||||
{
|
||||
if (TryGetStringProperty(checkpointObject, "rootHash", out var checkpointRootHash))
|
||||
{
|
||||
rootHashString = checkpointRootHash;
|
||||
}
|
||||
else if (TryGetStringProperty(checkpointObject, "hash", out var checkpointHash))
|
||||
{
|
||||
rootHashString = checkpointHash;
|
||||
}
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(rootHashString))
|
||||
{
|
||||
error = "missing rootHash";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryDecodeHashValue(rootHashString, out rootHash))
|
||||
{
|
||||
error = "invalid rootHash format";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryResolveProofHashes(JsonElement root, out List<byte[]> hashes, out string? error)
|
||||
{
|
||||
hashes = new List<byte[]>();
|
||||
error = null;
|
||||
|
||||
JsonElement hashesElement;
|
||||
if (TryGetArrayProperty(root, "hashes", out hashesElement) ||
|
||||
(TryGetObjectProperty(root, "inclusion", out var inclusion) && TryGetArrayProperty(inclusion, "hashes", out hashesElement)) ||
|
||||
(TryGetObjectProperty(root, "inclusion", out inclusion) && TryGetArrayProperty(inclusion, "path", out hashesElement)) ||
|
||||
(TryGetObjectProperty(root, "inclusionProof", out var inclusionProof) && TryGetArrayProperty(inclusionProof, "hashes", out hashesElement)) ||
|
||||
(TryGetObjectProperty(root, "inclusionProof", out inclusionProof) && TryGetArrayProperty(inclusionProof, "path", out hashesElement)))
|
||||
{
|
||||
foreach (var hashElement in hashesElement.EnumerateArray())
|
||||
{
|
||||
if (hashElement.ValueKind != JsonValueKind.String)
|
||||
{
|
||||
error = "hash entry is not a string";
|
||||
return false;
|
||||
}
|
||||
|
||||
var hashText = hashElement.GetString();
|
||||
if (string.IsNullOrWhiteSpace(hashText))
|
||||
{
|
||||
error = "hash entry is empty";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryDecodeHashValue(hashText, out var hashBytes))
|
||||
{
|
||||
error = $"invalid hash entry: {hashText}";
|
||||
return false;
|
||||
}
|
||||
|
||||
hashes.Add(hashBytes);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
error = "hashes/path array missing";
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryResolveLeafHash(JsonElement root, out byte[] leafHash, out string? error)
|
||||
{
|
||||
leafHash = Array.Empty<byte>();
|
||||
error = null;
|
||||
|
||||
if (TryGetStringProperty(root, "leafHash", out var directLeafHash) &&
|
||||
TryDecodeHashValue(directLeafHash, out leafHash))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryGetObjectProperty(root, "inclusion", out var inclusion) &&
|
||||
TryGetStringProperty(inclusion, "leafHash", out var inclusionLeafHash) &&
|
||||
TryDecodeHashValue(inclusionLeafHash, out leafHash))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (TryGetObjectProperty(root, "inclusionProof", out var inclusionProof) &&
|
||||
TryGetStringProperty(inclusionProof, "leafHash", out var inclusionProofLeafHash) &&
|
||||
TryDecodeHashValue(inclusionProofLeafHash, out leafHash))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
error = "leafHash missing";
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryDecodeHashValue(string value, out byte[] hashBytes)
|
||||
{
|
||||
hashBytes = Array.Empty<byte>();
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = value.Trim();
|
||||
if (normalized.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
normalized = normalized["sha256:".Length..];
|
||||
}
|
||||
|
||||
if (normalized.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
normalized = normalized[2..];
|
||||
}
|
||||
|
||||
if (normalized.Length == 64 && normalized.All(IsHexChar))
|
||||
{
|
||||
try
|
||||
{
|
||||
hashBytes = Convert.FromHexString(normalized);
|
||||
return hashBytes.Length == 32;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var base64Bytes = Convert.FromBase64String(normalized);
|
||||
if (base64Bytes.Length == 32)
|
||||
{
|
||||
hashBytes = base64Bytes;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Not base64.
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsHexChar(char value)
|
||||
{
|
||||
return (value >= '0' && value <= '9') ||
|
||||
(value >= 'a' && value <= 'f') ||
|
||||
(value >= 'A' && value <= 'F');
|
||||
}
|
||||
|
||||
private static bool TryGetInt64Property(JsonElement element, string propertyName, out long value)
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.Object &&
|
||||
element.TryGetProperty(propertyName, out var property))
|
||||
{
|
||||
if (property.ValueKind == JsonValueKind.Number && property.TryGetInt64(out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (property.ValueKind == JsonValueKind.String &&
|
||||
long.TryParse(property.GetString(), NumberStyles.Integer, CultureInfo.InvariantCulture, out value))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetStringProperty(JsonElement element, string propertyName, out string value)
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.Object &&
|
||||
element.TryGetProperty(propertyName, out var property) &&
|
||||
property.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
var text = property.GetString();
|
||||
if (!string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
value = text;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetArrayProperty(JsonElement element, string propertyName, out JsonElement value)
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.Object &&
|
||||
element.TryGetProperty(propertyName, out value) &&
|
||||
value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetObjectProperty(JsonElement element, string propertyName, out JsonElement value)
|
||||
{
|
||||
if (element.ValueKind == JsonValueKind.Object &&
|
||||
element.TryGetProperty(propertyName, out value) &&
|
||||
value.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool VerifyPayloadTypes(
|
||||
BundleManifestDto? manifest,
|
||||
VerificationResult result,
|
||||
@@ -1391,12 +1963,21 @@ public static class BundleVerifyCommand
|
||||
{
|
||||
[JsonPropertyName("signatures")]
|
||||
public List<SignatureDto>? Signatures { get; set; }
|
||||
|
||||
[JsonPropertyName("payload")]
|
||||
public string? Payload { get; set; }
|
||||
|
||||
[JsonPropertyName("payloadType")]
|
||||
public string? PayloadType { get; set; }
|
||||
}
|
||||
|
||||
private sealed class SignatureDto
|
||||
{
|
||||
[JsonPropertyName("keyid")]
|
||||
public string? KeyId { get; set; }
|
||||
|
||||
[JsonPropertyName("sig")]
|
||||
public string? Sig { get; set; }
|
||||
}
|
||||
|
||||
private sealed class RekorProofDto
|
||||
@@ -1414,5 +1995,7 @@ public static class BundleVerifyCommand
|
||||
public string? RootHash { get; set; }
|
||||
}
|
||||
|
||||
private readonly record struct ParsedCheckpoint(long TreeSize, byte[] RootHash);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user