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));
}
}