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