Publish curated Knowledge Hub source from 2ed833f290b439a7bc91e5fd19b042c269e3bee9
This commit is contained in:
1 parent
10f2edcd29
commit
59f15b2d08
218 files changed
+23995
-1849
No files matched your search
@@ -0,0 +1,185 @@
|
||||
using System.Globalization;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using ItWorx.KnowledgeHub.Application.Operations;
|
||||
using ItWorx.KnowledgeHub.Infrastructure.Operations;
|
||||
|
||||
namespace ItWorx.KnowledgeHub.IntegrationTests.Operations;
|
||||
|
||||
public sealed class BackupEvidenceTests : IDisposable
|
||||
{
|
||||
private readonly string root = Path.Combine(Path.GetTempPath(), "kh-evidence-" + Guid.NewGuid().ToString("N"));
|
||||
private string BackupsRoot => Path.Combine(root, "backups");
|
||||
private string KeysRoot => Path.Combine(root, "keys");
|
||||
|
||||
[Fact]
|
||||
public async Task Historical_manifest_has_unknown_integrity_and_restore_until_actual_evidence_is_recorded()
|
||||
{
|
||||
var store = CreateStore("backup-one");
|
||||
var initial = await store.GetAsync("backup-one", CancellationToken.None);
|
||||
Assert.NotNull(initial);
|
||||
Assert.Null(initial.IntegrityVerifiedAt);
|
||||
Assert.Null(initial.RestoreVerifiedAt);
|
||||
Assert.False(initial.Protected);
|
||||
|
||||
await WriteValidIntegrityAsync("backup-one");
|
||||
await store.RecordRestoreAsync("backup-one", new RestoreReport("backup-one", true,
|
||||
[new RestoreTableReport("pages", 3, 3)], 1, 1, [], ManifestHash("backup-one")), CancellationToken.None);
|
||||
var verified = await store.GetAsync("backup-one", CancellationToken.None);
|
||||
Assert.NotNull(verified!.RestoreVerifiedAt);
|
||||
// Changing the manifest invalidates previously verified evidence.
|
||||
await File.AppendAllTextAsync(Path.Combine(BackupsRoot, "backup-one", "manifest.json"), " ");
|
||||
Assert.Null((await store.GetAsync("backup-one", CancellationToken.None))!.RestoreVerifiedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Manifest_changed_during_restore_cannot_receive_successful_receipt()
|
||||
{
|
||||
var store = CreateStore("backup-changed");
|
||||
await WriteValidIntegrityAsync("backup-changed");
|
||||
var digest = ManifestHash("backup-changed");
|
||||
await File.AppendAllTextAsync(Path.Combine(BackupsRoot, "backup-changed", "manifest.json"), " ");
|
||||
await Assert.ThrowsAsync<IOException>(() => store.RecordRestoreAsync("backup-changed",
|
||||
new RestoreReport("backup-changed", true, [], 0, 0, [], digest), CancellationToken.None));
|
||||
Assert.Null((await store.GetAsync("backup-changed", CancellationToken.None))!.RestoreVerifiedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Integrity_evidence_is_bound_to_the_exact_manifest_hash_used_for_staging()
|
||||
{
|
||||
const string backupId = "backup-bound-manifest";
|
||||
var store = CreateStore(backupId);
|
||||
await WriteValidIntegrityAsync(backupId);
|
||||
var manifestHash = ManifestHash(backupId);
|
||||
|
||||
var matching = await store.GetAsync(backupId, CancellationToken.None, manifestHash);
|
||||
var mismatching = await store.GetAsync(backupId, CancellationToken.None, new string('0', 64));
|
||||
|
||||
Assert.NotNull(matching);
|
||||
Assert.NotNull(matching.IntegrityVerifiedAt);
|
||||
Assert.NotNull(mismatching);
|
||||
Assert.Null(mismatching.IntegrityVerifiedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Forged_manifest_and_matching_unsigned_receipt_do_not_verify()
|
||||
{
|
||||
const string backupId = "backup-forged";
|
||||
var store = CreateStore(backupId);
|
||||
await WriteValidIntegrityAsync(backupId);
|
||||
|
||||
var manifestPath = Path.Combine(BackupsRoot, backupId, "manifest.json");
|
||||
var manifest = JsonSerializer.Deserialize<BackupManifest>(await File.ReadAllTextAsync(manifestPath));
|
||||
Assert.NotNull(manifest);
|
||||
await File.WriteAllTextAsync(manifestPath, JsonSerializer.Serialize(manifest with { ProductVersion = "forged" }));
|
||||
var changedHash = ManifestHash(backupId);
|
||||
var forgedReceipt = new
|
||||
{
|
||||
Version = 1,
|
||||
BackupId = backupId,
|
||||
ManifestSha256 = changedHash,
|
||||
CompletedAt = DateTimeOffset.UtcNow,
|
||||
Success = true,
|
||||
HmacSha256 = new string('0', 64)
|
||||
};
|
||||
await File.WriteAllTextAsync(Path.Combine(BackupsRoot, backupId, ".integrity.json"),
|
||||
JsonSerializer.Serialize(forgedReceipt));
|
||||
|
||||
var evidence = await store.GetAsync(backupId, CancellationToken.None);
|
||||
Assert.NotNull(evidence);
|
||||
Assert.Null(evidence.IntegrityVerifiedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Missing_integrity_key_invalidates_a_previously_signed_receipt()
|
||||
{
|
||||
const string backupId = "backup-key-lost";
|
||||
var store = CreateStore(backupId);
|
||||
await WriteValidIntegrityAsync(backupId);
|
||||
Assert.NotNull((await store.GetAsync(backupId, CancellationToken.None))!.IntegrityVerifiedAt);
|
||||
|
||||
File.Delete(Path.Combine(KeysRoot, "backup-integrity.key"));
|
||||
|
||||
Assert.Null((await store.GetAsync(backupId, CancellationToken.None))!.IntegrityVerifiedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Existing_unix_integrity_key_is_restricted_to_owner_read_write_before_use()
|
||||
{
|
||||
if (OperatingSystem.IsWindows()) return;
|
||||
|
||||
const string backupId = "backup-key-mode";
|
||||
var store = CreateStore(backupId);
|
||||
await WriteValidIntegrityAsync(backupId);
|
||||
var keyPath = Path.Combine(KeysRoot, "backup-integrity.key");
|
||||
File.SetUnixFileMode(keyPath,
|
||||
UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.GroupRead | UnixFileMode.OtherRead);
|
||||
|
||||
var evidence = await store.GetAsync(backupId, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(evidence);
|
||||
Assert.NotNull(evidence.IntegrityVerifiedAt);
|
||||
Assert.Equal(UnixFileMode.UserRead | UnixFileMode.UserWrite, File.GetUnixFileMode(keyPath));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Protection_is_versioned_and_stale_write_cannot_remove_it()
|
||||
{
|
||||
var store = CreateStore("backup-two");
|
||||
var protectedBackup = await store.SetProtectionAsync("backup-two", true, 0, Guid.NewGuid(), CancellationToken.None);
|
||||
Assert.Equal(BackupProtectionResult.Updated, protectedBackup);
|
||||
Assert.Equal(BackupProtectionResult.Conflict,
|
||||
await store.SetProtectionAsync("backup-two", false, 0, Guid.NewGuid(), CancellationToken.None));
|
||||
var state = await store.GetAsync("backup-two", CancellationToken.None);
|
||||
Assert.True(state!.Protected);
|
||||
Assert.Equal(1, state.ProtectionVersion);
|
||||
Assert.Null(await store.GetAsync("../escape", CancellationToken.None));
|
||||
}
|
||||
|
||||
private FileBackupEvidenceStore CreateStore(string id)
|
||||
{
|
||||
Directory.CreateDirectory(Path.Combine(BackupsRoot, id));
|
||||
File.WriteAllText(Path.Combine(BackupsRoot, id, "manifest.json"), JsonSerializer.Serialize(
|
||||
new BackupManifest(id, DateTimeOffset.UtcNow, "Hub", "1.0", "development", "schema", [], [], 0)));
|
||||
return new FileBackupEvidenceStore(new BackupPaths(BackupsRoot, Path.Combine(root, "attachments"), KeysRoot), TimeProvider.System);
|
||||
}
|
||||
|
||||
private string ManifestHash(string id) => Convert.ToHexStringLower(
|
||||
SHA256.HashData(File.ReadAllBytes(Path.Combine(BackupsRoot, id, "manifest.json"))));
|
||||
|
||||
private async Task WriteValidIntegrityAsync(string id)
|
||||
{
|
||||
Directory.CreateDirectory(KeysRoot);
|
||||
var keyPath = Path.Combine(KeysRoot, "backup-integrity.key");
|
||||
if (!File.Exists(keyPath))
|
||||
await File.WriteAllBytesAsync(keyPath, RandomNumberGenerator.GetBytes(32));
|
||||
|
||||
var key = await File.ReadAllBytesAsync(keyPath);
|
||||
var completedAt = DateTimeOffset.UtcNow;
|
||||
var manifestHash = ManifestHash(id);
|
||||
var payload = string.Join('\n',
|
||||
"itworx-knowledge-hub-backup-evidence-v1",
|
||||
"integrity",
|
||||
id,
|
||||
manifestHash,
|
||||
completedAt.UtcTicks.ToString(CultureInfo.InvariantCulture),
|
||||
"1");
|
||||
var record = new
|
||||
{
|
||||
Version = 1,
|
||||
BackupId = id,
|
||||
ManifestSha256 = manifestHash,
|
||||
CompletedAt = completedAt,
|
||||
Success = true,
|
||||
HmacSha256 = Convert.ToHexStringLower(HMACSHA256.HashData(key, Encoding.UTF8.GetBytes(payload)))
|
||||
};
|
||||
await File.WriteAllTextAsync(Path.Combine(BackupsRoot, id, ".integrity.json"),
|
||||
JsonSerializer.Serialize(record));
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Directory.Exists(root)) Directory.Delete(root, recursive: true);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user