namespace DodoSSH.Crypto.Tests; /// /// Asserts the committed golden vectors still hold. /// /// /// /// This is the single most important test in the product. The server holds ciphertext and no /// keys, so it can never re-encrypt anything: a change to the envelope layout or to AAD /// derivation that reaches a release makes every existing vault undecryptable, with no /// server-side remedy and no rollback. /// /// /// A failure here is never fixed by regenerating the fixture. It means either a genuine /// regression, or an intentional format change — which requires a new /// aadVersion/algId and a client-side lazy re-encrypt-on-write path to exist /// first. See docs/crypto.md §8. /// /// /// To regenerate deliberately, set DODOSSH_REGENERATE_VECTORS=1. The test rewrites the /// fixture in the source tree and then fails, so the diff has to be reviewed rather than /// silently absorbed. /// /// public sealed class GoldenVectorTests { private const string RegenerateVariable = "DODOSSH_REGENERATE_VECTORS"; private const string FixtureRelativePath = "fixtures/crypto/vectors.json"; [Fact] public void CommittedVectors_MatchCurrentImplementation() { var actual = GoldenVectors.Generate(); if (string.Equals(Environment.GetEnvironmentVariable(RegenerateVariable), "1", StringComparison.Ordinal)) { var sourcePath = ResolveSourceTreeFixturePath(); Directory.CreateDirectory(Path.GetDirectoryName(sourcePath)!); File.WriteAllText(sourcePath, actual); Assert.Fail( $"Regenerated {sourcePath}. Review the diff and unset {RegenerateVariable}. " + "If the envelope or AAD changed, a version bump and a client migration path are required first."); } var expected = File.ReadAllText(OutputFixturePath()); Normalise(actual).ShouldBe( Normalise(expected), "The DSH1 format or AAD derivation changed. This would make every existing vault " + "undecryptable. Do not regenerate the fixture to silence this."); } [Fact] public void Fixture_IsCommittedAndNonTrivial() { var content = File.ReadAllText(OutputFixturePath()); content.Length.ShouldBeGreaterThan(1000); content.ShouldContain("canonicalEncoding"); content.ShouldContain("\"specVersion\": 1"); } private static string Normalise(string json) => json.ReplaceLineEndings("\n").TrimEnd(); /// /// The fixture as copied beside the test assembly. Robust under deterministic source paths. /// private static string OutputFixturePath() { var path = Path.Combine(AppContext.BaseDirectory, FixtureRelativePath); File.Exists(path).ShouldBeTrue( $"Golden vector fixture missing at {path}. It should be copied to the output " + $"directory by the project file. Set {RegenerateVariable}=1 to create it."); return path; } /// /// Locates the fixture in the source tree by walking up to the solution file. /// /// /// Used only when regenerating, which is a developer-local action. /// private static string ResolveSourceTreeFixturePath() { var directory = new DirectoryInfo(AppContext.BaseDirectory); while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "DodoSSH.slnx"))) { directory = directory.Parent; } if (directory is null) { throw new InvalidOperationException( "Could not locate the repository root (no DodoSSH.slnx found above " + $"{AppContext.BaseDirectory}). Regenerate from within the repository."); } return Path.Combine(directory.FullName, "tests", FixtureRelativePath.Replace('/', Path.DirectorySeparatorChar)); } }