using System.Security.Cryptography;
using NSec.Cryptography;
namespace DodoSSH.Client.Ssh;
/// Which kind of key pair to make.
public enum SshKeyAlgorithm
{
/// Ed25519. Small, fast, and what every current OpenSSH prefers.
Ed25519 = 0,
/// RSA at 4096 bits, for servers too old to accept the above.
Rsa4096 = 1,
}
///
/// A freshly generated key pair, in the two forms anybody needs it in.
///
///
/// The private half, in the armoured form ssh-keygen writes. Goes straight into
/// SshKeySecret.PrivateKeyPem, which stores it verbatim.
///
///
/// The public half, as one authorized_keys line. This is what gets installed on a host.
///
///
/// The SHA256:… fingerprint, in the format ssh-keygen -lf prints, so it can be read out to
/// somebody or compared against what a host reports.
///
public sealed record GeneratedSshKey(string PrivateKeyArmour, string PublicKeyLine, string Fingerprint);
///
/// Makes a new SSH key pair without shelling out to ssh-keygen.
///
///
///
/// Why the client can do this at all. Every part is already here: NSec does Ed25519 because .NET
/// does not, the BCL does RSA, and the SSH wire encoding is a few length-prefixed strings — see
/// . What it buys is that the private key is never written to a disk. The
/// alternative flow is "run ssh-keygen, find the file, open it, copy the text, paste it here, remember to
/// delete the file", and the last step is the one nobody does.
///
///
/// The armour has no passphrase, and that is a deliberate limitation with its reasoning in
/// . The key is protected by the keychain it lands in.
///
///
/// This lives in the SSH project rather than in DodoSSH.Crypto, which is the normative
/// implementation of docs/crypto.md and has nothing to say about SSH file formats. It is also where
/// already lives, and a second SHA256: encoder would be a second
/// thing to get wrong.
///
///
public static class SshKeyGenerator
{
///
/// Generates a key pair.
///
/// Which kind.
///
/// The trailing comment, conventionally user@machine. It identifies the key in a host's
/// authorized_keys and is the only thing there that will say where it came from.
///
///
/// Synchronous and CPU-bound. RSA at 4096 bits is seconds of work on an ordinary machine, so a caller on
/// a UI thread has to move this to one of its own — the window would otherwise freeze at exactly the
/// moment somebody is watching it. Ed25519 is effectively instant, and the caller should not have to
/// know which is which.
///
public static GeneratedSshKey Generate(SshKeyAlgorithm algorithm, string comment) => algorithm switch
{
SshKeyAlgorithm.Ed25519 => Ed25519(comment),
SshKeyAlgorithm.Rsa4096 => Rsa4096(comment),
_ => throw new ArgumentOutOfRangeException(nameof(algorithm)),
};
private static GeneratedSshKey Ed25519(string comment)
{
var parameters = new KeyCreationParameters
{
// The seed has to come back out to be written into the file. NSec holds key material in
// libsodium's guarded memory and refuses to export it unless asked at creation time.
ExportPolicy = KeyExportPolicies.AllowPlaintextExport,
};
using var key = Key.Create(SignatureAlgorithm.Ed25519, parameters);
var seed = key.Export(KeyBlobFormat.RawPrivateKey);
try
{
var publicKey = key.PublicKey.Export(KeyBlobFormat.RawPublicKey);
return new GeneratedSshKey(
OpenSshKeyWriter.WriteEd25519PrivateKey(seed, publicKey, comment),
OpenSshKeyWriter.WriteEd25519PublicKey(publicKey, comment),
SshHostKeyFingerprint.Format(OpenSshKeyWriter.Ed25519PublicBlob(publicKey)));
}
finally
{
// The one copy of the private scalar this method makes, and it is an ordinary managed array
// outside libsodium's guarded memory. Clearing it does not undo anything the garbage collector
// may already have moved, which is why the export happens once and is used immediately.
CryptographicOperations.ZeroMemory(seed);
}
}
///
/// PKCS#1, which is what ExportRSAPrivateKeyPem writes and what SSH.NET's RSA PRIVATE KEY
/// branch reads. No hand-encoding is needed on this path at all — only the public line, because there is
/// no BCL helper for the SSH wire format.
///
private static GeneratedSshKey Rsa4096(string comment)
{
using var rsa = RSA.Create(4096);
return new GeneratedSshKey(
rsa.ExportRSAPrivateKeyPem() + "\n",
OpenSshKeyWriter.WriteRsaPublicKey(rsa, comment),
SshHostKeyFingerprint.Format(OpenSshKeyWriter.RsaPublicBlob(rsa)));
}
}