work work hard work
This commit is contained in:
@@ -286,6 +286,8 @@ internal static partial class CommandHandlers
|
||||
}
|
||||
}
|
||||
|
||||
var dssePath = (verifyDsse || verifyRekor) ? ResolveOfflineDssePath(bundleDir) : null;
|
||||
|
||||
var dsseVerified = false;
|
||||
if (verifyDsse)
|
||||
{
|
||||
@@ -304,7 +306,6 @@ internal static partial class CommandHandlers
|
||||
return;
|
||||
}
|
||||
|
||||
var dssePath = ResolveOfflineDssePath(bundleDir);
|
||||
if (dssePath is null)
|
||||
{
|
||||
verificationLog.Add("dsse:missing");
|
||||
@@ -507,6 +508,44 @@ internal static partial class CommandHandlers
|
||||
var rekorVerified = false;
|
||||
if (verifyRekor)
|
||||
{
|
||||
if (dssePath is null)
|
||||
{
|
||||
verificationLog.Add("rekor:missing-dsse");
|
||||
var quarantineId = await TryQuarantineOfflineBundleAsync(
|
||||
loggerFactory,
|
||||
quarantineRoot,
|
||||
effectiveTenant,
|
||||
bundlePath,
|
||||
manifestJson,
|
||||
reasonCode: "REKOR_VERIFY_FAIL",
|
||||
reasonMessage: "Rekor verification requires a DSSE statement file (statement.dsse.json).",
|
||||
verificationLog,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await WriteOfflineImportResultAsync(
|
||||
emitJson,
|
||||
new OfflineImportResultPayload(
|
||||
Status: "failed",
|
||||
ExitCode: OfflineExitCodes.RekorVerificationFailed,
|
||||
TenantId: effectiveTenant,
|
||||
BundlePath: bundlePath,
|
||||
ManifestPath: manifestPath,
|
||||
Version: manifest.Version,
|
||||
Digest: $"sha256:{bundleDigest}",
|
||||
DsseVerified: dsseVerified,
|
||||
RekorVerified: false,
|
||||
ActivatedAt: null,
|
||||
WasForceActivated: false,
|
||||
ForceActivateReason: null,
|
||||
QuarantineId: quarantineId,
|
||||
ReasonCode: "REKOR_VERIFY_FAIL",
|
||||
ReasonMessage: "Rekor verification requires a DSSE statement file (statement.dsse.json)."),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Environment.ExitCode = OfflineExitCodes.RekorVerificationFailed;
|
||||
return;
|
||||
}
|
||||
|
||||
var rekorPath = ResolveOfflineRekorReceiptPath(bundleDir);
|
||||
if (rekorPath is null)
|
||||
{
|
||||
@@ -546,20 +585,10 @@ internal static partial class CommandHandlers
|
||||
return;
|
||||
}
|
||||
|
||||
var receiptJson = await File.ReadAllTextAsync(rekorPath, cancellationToken).ConfigureAwait(false);
|
||||
var receipt = JsonSerializer.Deserialize<OfflineKitRekorReceiptDocument>(receiptJson, new JsonSerializerOptions(JsonSerializerDefaults.Web)
|
||||
var rekorKeyPath = ResolveOfflineRekorPublicKeyPath(bundleDir);
|
||||
if (rekorKeyPath is null)
|
||||
{
|
||||
PropertyNameCaseInsensitive = true
|
||||
});
|
||||
|
||||
if (receipt is null ||
|
||||
string.IsNullOrWhiteSpace(receipt.Uuid) ||
|
||||
receipt.LogIndex < 0 ||
|
||||
string.IsNullOrWhiteSpace(receipt.RootHash) ||
|
||||
receipt.Hashes is not { Count: > 0 } ||
|
||||
string.IsNullOrWhiteSpace(receipt.Checkpoint))
|
||||
{
|
||||
verificationLog.Add("rekor:invalid");
|
||||
verificationLog.Add("rekor:missing-public-key");
|
||||
var quarantineId = await TryQuarantineOfflineBundleAsync(
|
||||
loggerFactory,
|
||||
quarantineRoot,
|
||||
@@ -567,7 +596,7 @@ internal static partial class CommandHandlers
|
||||
bundlePath,
|
||||
manifestJson,
|
||||
reasonCode: "REKOR_VERIFY_FAIL",
|
||||
reasonMessage: "Rekor receipt is missing required fields.",
|
||||
reasonMessage: "Rekor public key not found in offline bundle (rekor-pub.pem).",
|
||||
verificationLog,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -588,16 +617,26 @@ internal static partial class CommandHandlers
|
||||
ForceActivateReason: null,
|
||||
QuarantineId: quarantineId,
|
||||
ReasonCode: "REKOR_VERIFY_FAIL",
|
||||
ReasonMessage: "Rekor receipt is missing required fields."),
|
||||
ReasonMessage: "Rekor public key not found in offline bundle (rekor-pub.pem)."),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Environment.ExitCode = OfflineExitCodes.RekorVerificationFailed;
|
||||
return;
|
||||
}
|
||||
|
||||
if (receipt.Checkpoint.IndexOf(receipt.RootHash, StringComparison.OrdinalIgnoreCase) < 0)
|
||||
var dsseBytes = await File.ReadAllBytesAsync(dssePath, cancellationToken).ConfigureAwait(false);
|
||||
var dsseSha256 = SHA256.HashData(dsseBytes);
|
||||
|
||||
var verify = await RekorOfflineReceiptVerifier.VerifyAsync(
|
||||
rekorPath,
|
||||
dsseSha256,
|
||||
rekorKeyPath,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (!verify.Verified)
|
||||
{
|
||||
verificationLog.Add("rekor:checkpoint-mismatch");
|
||||
verificationLog.Add("rekor:verify-failed");
|
||||
var quarantineId = await TryQuarantineOfflineBundleAsync(
|
||||
loggerFactory,
|
||||
quarantineRoot,
|
||||
@@ -605,7 +644,7 @@ internal static partial class CommandHandlers
|
||||
bundlePath,
|
||||
manifestJson,
|
||||
reasonCode: "REKOR_VERIFY_FAIL",
|
||||
reasonMessage: "Rekor checkpoint does not reference receipt rootHash.",
|
||||
reasonMessage: verify.FailureReason ?? "Rekor verification failed.",
|
||||
verificationLog,
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -626,7 +665,7 @@ internal static partial class CommandHandlers
|
||||
ForceActivateReason: null,
|
||||
QuarantineId: quarantineId,
|
||||
ReasonCode: "REKOR_VERIFY_FAIL",
|
||||
ReasonMessage: "Rekor checkpoint does not reference receipt rootHash."),
|
||||
ReasonMessage: verify.FailureReason ?? "Rekor verification failed."),
|
||||
cancellationToken).ConfigureAwait(false);
|
||||
|
||||
Environment.ExitCode = OfflineExitCodes.RekorVerificationFailed;
|
||||
@@ -635,8 +674,15 @@ internal static partial class CommandHandlers
|
||||
|
||||
rekorVerified = true;
|
||||
verificationLog.Add("rekor:ok");
|
||||
activity?.SetTag("stellaops.cli.offline.rekor_uuid", receipt.Uuid);
|
||||
activity?.SetTag("stellaops.cli.offline.rekor_log_index", receipt.LogIndex);
|
||||
if (!string.IsNullOrWhiteSpace(verify.RekorUuid))
|
||||
{
|
||||
activity?.SetTag("stellaops.cli.offline.rekor_uuid", verify.RekorUuid);
|
||||
}
|
||||
|
||||
if (verify.LogIndex is not null)
|
||||
{
|
||||
activity?.SetTag("stellaops.cli.offline.rekor_log_index", verify.LogIndex.Value);
|
||||
}
|
||||
}
|
||||
|
||||
BundleVersion incomingVersion;
|
||||
@@ -947,6 +993,25 @@ internal static partial class CommandHandlers
|
||||
return candidates.FirstOrDefault(File.Exists);
|
||||
}
|
||||
|
||||
private static string? ResolveOfflineRekorPublicKeyPath(string bundleDirectory)
|
||||
{
|
||||
var candidates = new[]
|
||||
{
|
||||
Path.Combine(bundleDirectory, "rekor-pub.pem"),
|
||||
Path.Combine(bundleDirectory, "rekor.pub"),
|
||||
Path.Combine(bundleDirectory, "tlog-root.pub"),
|
||||
Path.Combine(bundleDirectory, "tlog-root.pem"),
|
||||
Path.Combine(bundleDirectory, "tlog", "rekor-pub.pem"),
|
||||
Path.Combine(bundleDirectory, "tlog", "rekor.pub"),
|
||||
Path.Combine(bundleDirectory, "keys", "tlog-root", "rekor-pub.pem"),
|
||||
Path.Combine(bundleDirectory, "keys", "tlog-root", "rekor.pub"),
|
||||
Path.Combine(bundleDirectory, "evidence", "keys", "tlog-root", "rekor-pub.pem"),
|
||||
Path.Combine(bundleDirectory, "evidence", "keys", "tlog-root", "rekor.pub"),
|
||||
};
|
||||
|
||||
return candidates.FirstOrDefault(File.Exists);
|
||||
}
|
||||
|
||||
private static async Task<byte[]> LoadTrustRootPublicKeyAsync(string path, CancellationToken cancellationToken)
|
||||
{
|
||||
var bytes = await File.ReadAllBytesAsync(path, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -121,15 +121,58 @@ public sealed class OfflineCommandHandlersTests
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true });
|
||||
await File.WriteAllTextAsync(dssePath, dsseJson, CancellationToken.None);
|
||||
|
||||
var rootHash = "deadbeef";
|
||||
static byte[] HashLeaf(byte[] leafData)
|
||||
{
|
||||
var buffer = new byte[1 + leafData.Length];
|
||||
buffer[0] = 0x00;
|
||||
leafData.CopyTo(buffer, 1);
|
||||
return SHA256.HashData(buffer);
|
||||
}
|
||||
|
||||
static byte[] HashInterior(byte[] left, byte[] right)
|
||||
{
|
||||
var buffer = new byte[1 + left.Length + right.Length];
|
||||
buffer[0] = 0x01;
|
||||
left.CopyTo(buffer, 1);
|
||||
right.CopyTo(buffer, 1 + left.Length);
|
||||
return SHA256.HashData(buffer);
|
||||
}
|
||||
|
||||
// Deterministic DSSE digest used as the Rekor leaf input.
|
||||
var dsseBytes = await File.ReadAllBytesAsync(dssePath, CancellationToken.None);
|
||||
var dsseSha256 = SHA256.HashData(dsseBytes);
|
||||
|
||||
// Build a minimal 2-leaf RFC6962 Merkle tree proof for logIndex=0.
|
||||
var leaf0 = HashLeaf(dsseSha256);
|
||||
var leaf1 = HashLeaf(SHA256.HashData(Encoding.UTF8.GetBytes("other-envelope")));
|
||||
var rootHashBytes = HashInterior(leaf0, leaf1);
|
||||
|
||||
using var rekorKey = ECDsa.Create(ECCurve.NamedCurves.nistP256);
|
||||
var checkpointOrigin = "rekor.sigstore.dev - 2605736670972794746";
|
||||
var checkpointTimestamp = "1700000000";
|
||||
var checkpointBody = $"{checkpointOrigin}\n2\n{Convert.ToBase64String(rootHashBytes)}\n{checkpointTimestamp}\n";
|
||||
var checkpointSig = rekorKey.SignData(Encoding.UTF8.GetBytes(checkpointBody), HashAlgorithmName.SHA256);
|
||||
|
||||
var rekorPublicKeyPath = Path.Combine(bundleDir, "rekor-pub.pem");
|
||||
await File.WriteAllTextAsync(
|
||||
rekorPublicKeyPath,
|
||||
WrapPem("PUBLIC KEY", rekorKey.ExportSubjectPublicKeyInfo()),
|
||||
CancellationToken.None);
|
||||
|
||||
var checkpointPath = Path.Combine(bundleDir, "checkpoint.sig");
|
||||
await File.WriteAllTextAsync(
|
||||
checkpointPath,
|
||||
checkpointBody + $"sig {Convert.ToBase64String(checkpointSig)}\n",
|
||||
CancellationToken.None);
|
||||
|
||||
var rekorPath = Path.Combine(bundleDir, "rekor-receipt.json");
|
||||
var rekorJson = JsonSerializer.Serialize(new
|
||||
{
|
||||
uuid = "rekor-test",
|
||||
logIndex = 42,
|
||||
rootHash,
|
||||
hashes = new[] { "hash-1" },
|
||||
checkpoint = $"checkpoint {rootHash}"
|
||||
logIndex = 0,
|
||||
rootHash = Convert.ToHexString(rootHashBytes).ToLowerInvariant(),
|
||||
hashes = new[] { Convert.ToHexString(leaf1).ToLowerInvariant() },
|
||||
checkpoint = "checkpoint.sig"
|
||||
}, new JsonSerializerOptions(JsonSerializerDefaults.Web) { WriteIndented = true });
|
||||
await File.WriteAllTextAsync(rekorPath, rekorJson, CancellationToken.None);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user