using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using DodoSSH.Crypto;
using NSec.Cryptography;
namespace DodoSSH.Crypto.Tests;
///
/// Produces the deterministic byte-level results of the DSH1 specification.
///
///
/// Only deterministic operations belong here. SealTo and signing draw fresh randomness,
/// so they are covered by round-trip and negative tests in instead.
///
internal static class GoldenVectors
{
private static readonly Guid ResourceA = Guid.Parse("0192f0c8-1a2b-7c3d-8e4f-5a6b7c8d9e0f");
private static readonly Guid ResourceB = Guid.Parse("0192f0c8-1a2b-7c3d-8e4f-5a6b7c8d9e10");
private static readonly Guid KeyIdA = Guid.Parse("0192f0c8-9999-7aaa-8bbb-cccccccccccc");
internal static string Generate()
{
var root = new JsonObject
{
["_comment"] = "Generated by DodoSSH.Crypto.Tests.GoldenVectors. Normative source: docs/crypto.md.",
["_warning"] = "A failing assertion here is a regression or an intentional versioned format change. Do not regenerate to make it pass.",
["specVersion"] = 1,
["aad"] = BuildAadVectors(),
["envelope"] = BuildEnvelopeVectors(),
["aead"] = BuildAeadVectors(),
["hkdf"] = BuildHkdfVectors(),
["argon2id"] = BuildArgon2Vectors(),
["fingerprint"] = BuildFingerprintVectors(),
};
return root.ToJsonString(new JsonSerializerOptions { WriteIndented = true }) + "\n";
}
private static JsonArray BuildAadVectors()
{
(string Name, AadDescriptor Descriptor)[] cases =
[
("item-payload", AadDescriptor.Create(
CryptoSpec.AadPurpose.ItemPayload,
CryptoSpec.AadResourceType.Credential,
ResourceA,
KeyIdA,
keyGeneration: 7,
itemVersion: 3)),
("item-payload-other-resource", AadDescriptor.Create(
CryptoSpec.AadPurpose.ItemPayload,
CryptoSpec.AadResourceType.Credential,
ResourceB,
KeyIdA,
keyGeneration: 7,
itemVersion: 3)),
("item-metadata", AadDescriptor.Create(
CryptoSpec.AadPurpose.ItemMetadata,
CryptoSpec.AadResourceType.Host,
ResourceA,
keyGeneration: 1,
itemVersion: 1)),
("vault-key-grant", AadDescriptor.Create(
CryptoSpec.AadPurpose.VaultKeyGrant,
CryptoSpec.AadResourceType.Vault,
ResourceA,
keyGeneration: 2)),
("user-secret-bundle", AadDescriptor.Create(
CryptoSpec.AadPurpose.UserSecretBundle,
CryptoSpec.AadResourceType.User,
ResourceA)),
("all-zero-ids", AadDescriptor.Create(
CryptoSpec.AadPurpose.LocalCache,
CryptoSpec.AadResourceType.None,
Guid.Empty,
keyGeneration: 0)),
];
var array = new JsonArray();
foreach (var (name, descriptor) in cases)
{
array.Add(new JsonObject
{
["name"] = name,
["purpose"] = (int)descriptor.Purpose,
["resourceType"] = (int)descriptor.ResourceType,
["resourceId"] = descriptor.ResourceId.ToString(),
["keyId"] = descriptor.KeyId.ToString(),
["keyGeneration"] = descriptor.KeyGeneration,
["itemVersion"] = descriptor.ItemVersion,
["aadVersion"] = descriptor.AadVersion,
["schemaVersion"] = descriptor.SchemaVersion,
["canonicalEncoding"] = Hex(descriptor.ToCanonicalEncoding()),
["aad"] = Hex(descriptor.ComputeAad()),
});
}
return array;
}
private static JsonArray BuildEnvelopeVectors()
{
// Framing only, with fixed inputs, so the byte layout of the header is pinned
// independently of any AEAD behaviour.
var ciphertext = Enumerable.Range(0, 20).Select(i => (byte)i).ToArray();
var xchachaNonce = Enumerable.Range(0, DshEnvelope.XChaChaNonceSize).Select(i => (byte)(0xA0 + i)).ToArray();
var gcmNonce = Enumerable.Range(0, DshEnvelope.AesGcmNonceSize).Select(i => (byte)(0xB0 + i)).ToArray();
var ephemeral = Enumerable.Range(0, CryptoSpec.PublicKeySize).Select(i => (byte)(0xC0 + i)).ToArray();
return
[
new JsonObject
{
["name"] = "xchacha20poly1305",
["algId"] = (int)CryptoSpec.AlgorithmId.XChaCha20Poly1305,
["nonce"] = Hex(xchachaNonce),
["ciphertext"] = Hex(ciphertext),
["prefixSize"] = DshEnvelope.PrefixSizeFor(CryptoSpec.AlgorithmId.XChaCha20Poly1305),
["envelope"] = Hex(DshEnvelope.Write(
CryptoSpec.AlgorithmId.XChaCha20Poly1305, xchachaNonce, ciphertext)),
},
new JsonObject
{
["name"] = "aes256gcm",
["algId"] = (int)CryptoSpec.AlgorithmId.Aes256Gcm,
["nonce"] = Hex(gcmNonce),
["ciphertext"] = Hex(ciphertext),
["prefixSize"] = DshEnvelope.PrefixSizeFor(CryptoSpec.AlgorithmId.Aes256Gcm),
["envelope"] = Hex(DshEnvelope.Write(
CryptoSpec.AlgorithmId.Aes256Gcm, gcmNonce, ciphertext)),
},
new JsonObject
{
["name"] = "sealto-x25519",
["algId"] = (int)CryptoSpec.AlgorithmId.SealToX25519,
["nonce"] = Hex(xchachaNonce),
["ephemeralPublicKey"] = Hex(ephemeral),
["ciphertext"] = Hex(ciphertext),
["prefixSize"] = DshEnvelope.PrefixSizeFor(CryptoSpec.AlgorithmId.SealToX25519),
["envelope"] = Hex(DshEnvelope.Write(
CryptoSpec.AlgorithmId.SealToX25519, xchachaNonce, ciphertext, ephemeral)),
},
];
}
private static JsonArray BuildAeadVectors()
{
// Fixed key and nonce, so the AEAD itself is pinned. DshCrypto.Seal draws a random
// nonce by design, so the primitive is exercised directly here.
var key = Enumerable.Range(0, CryptoSpec.SymmetricKeySize).Select(i => (byte)i).ToArray();
var nonce = Enumerable.Range(0, DshEnvelope.XChaChaNonceSize).Select(i => (byte)(0x10 + i)).ToArray();
var plaintext = "correct horse battery staple"u8.ToArray();
var descriptor = AadDescriptor.Create(
CryptoSpec.AadPurpose.ItemPayload,
CryptoSpec.AadResourceType.Credential,
ResourceA,
KeyIdA,
keyGeneration: 7,
itemVersion: 3);
var aad = descriptor.ComputeAad();
using var aeadKey = Key.Import(
AeadAlgorithm.XChaCha20Poly1305, key, KeyBlobFormat.RawSymmetricKey);
var ciphertext = AeadAlgorithm.XChaCha20Poly1305.Encrypt(aeadKey, nonce, aad, plaintext);
return
[
new JsonObject
{
["name"] = "xchacha20poly1305-with-canonical-aad",
["key"] = Hex(key),
["nonce"] = Hex(nonce),
["aad"] = Hex(aad),
["plaintext"] = Hex(plaintext),
["ciphertext"] = Hex(ciphertext),
["envelope"] = Hex(DshEnvelope.Write(
CryptoSpec.AlgorithmId.XChaCha20Poly1305, nonce, ciphertext)),
},
];
}
private static JsonArray BuildHkdfVectors()
{
var prk = Enumerable.Range(0, 64).Select(i => (byte)i).ToArray();
(string Name, byte[] Info)[] cases =
[
("passphrase-kek", CryptoSpec.DerivationLabels.PassphraseKek.ToArray()),
("local-cache", CryptoSpec.DerivationLabels.LocalCache.ToArray()),
];
var array = new JsonArray();
foreach (var (name, info) in cases)
{
array.Add(new JsonObject
{
["name"] = name,
["algorithm"] = "HKDF-SHA512-Expand",
["prk"] = Hex(prk),
["info"] = Encoding.UTF8.GetString(info),
["outputLength"] = CryptoSpec.SymmetricKeySize,
["output"] = Hex(HKDF.Expand(
HashAlgorithmName.SHA512, prk, CryptoSpec.SymmetricKeySize, info)),
});
}
return array;
}
private static JsonArray BuildArgon2Vectors()
{
var salt = Enumerable.Range(0, CryptoSpec.SaltSize).Select(i => (byte)(0x20 + i)).ToArray();
const string Passphrase = "correct horse battery staple";
var array = new JsonArray();
// Parameters of every profile are pinned cheaply. Only the smallest profile's output is
// computed, to keep the suite fast; the KDF itself is libsodium's, not ours.
(string Name, Argon2Profile Profile)[] profiles =
[
("passphrase-default", Argon2Profile.PassphraseDefault),
("passphrase-reduced", Argon2Profile.PassphraseReduced),
("passphrase-high", Argon2Profile.PassphraseHigh),
("random-secret", Argon2Profile.RandomSecret),
];
foreach (var (name, profile) in profiles)
{
array.Add(new JsonObject
{
["name"] = name,
["memoryMebibytes"] = profile.MemoryMebibytes,
["memoryKibibytes"] = profile.MemoryKibibytes,
["passes"] = profile.Passes,
["parallelism"] = Argon2Profile.Parallelism,
});
}
array.Add(new JsonObject
{
["name"] = "random-secret-output",
["memoryMebibytes"] = Argon2Profile.RandomSecret.MemoryMebibytes,
["memoryKibibytes"] = Argon2Profile.RandomSecret.MemoryKibibytes,
["passes"] = Argon2Profile.RandomSecret.Passes,
["parallelism"] = Argon2Profile.Parallelism,
["passphrase"] = Passphrase,
["salt"] = Hex(salt),
["outputLength"] = CryptoSpec.SymmetricKeySize,
["output"] = Hex(Argon2Profile.RandomSecret
.CreateAlgorithm()
.DeriveBytes(Passphrase, salt, CryptoSpec.SymmetricKeySize)),
});
return array;
}
private static JsonArray BuildFingerprintVectors()
{
var x25519 = Enumerable.Range(0, CryptoSpec.PublicKeySize).Select(i => (byte)(0x40 + i)).ToArray();
var ed25519 = Enumerable.Range(0, CryptoSpec.PublicKeySize).Select(i => (byte)(0x60 + i)).ToArray();
return
[
new JsonObject
{
["name"] = "identity-fingerprint",
["x25519PublicKey"] = Hex(x25519),
["ed25519PublicKey"] = Hex(ed25519),
["fingerprint"] = Hex(DshCrypto.ComputeFingerprint(x25519, ed25519)),
},
];
}
private static string Hex(ReadOnlySpan value) =>
Convert.ToHexString(value).ToLower(CultureInfo.InvariantCulture);
}