Implementing SSH host key cache (known hosts)

The following example shows how to implement a custom SSH host key cache, similar to the .ssh/known_hosts file of OpenSSH suite, using WinSCP .NET assembly.

If you do not need our own cache storage, you can instead use a similar built-in functionality using SshHostKeyPolicy.AcceptNew.

The example uses an XML file for the cache, as this format has native support in both in PowerShell and .NET framework. You can replace it with other format, if needed.

Advertisement

The format of the XML file is like:

<KnownHosts>
  <KnownHost host="example.com:22" fingerprint="ecdsa-sha2-nistp521 521 p3ZteKYBFsSyFh18yOaczZEqoXnn135qqH1VqdIzQ8k=" />
  <KnownHost host="example.org:22" fingerprint="ssh-rsa 2048 2EP3avJqmpRtSRaUIqwrzavm15vssrhHxJWh9mBaz8M=" />
</KnownHosts>

PowerShell

$KnownHostsFile = "KnownHosts.xml"
$SshPortNumber = 22
 
try
{
    # Load WinSCP .NET assembly
    Add-Type -Path "WinSCPnet.dll"
 
    # Setup session options
    $sessionOptions = New-Object WinSCP.SessionOptions -Property @{
        Protocol = [WinSCP.Protocol]::Sftp
        HostName = "example.com"
        UserName = "user"
        Password = "mypassword"
    }
 
    # Cache key is hostname:portnumber
    if ($sessionOptions.PortNumber -ne 0)
    {
        $portNumber = $sessionOptions.PortNumber
    }
    else
    {
        $portNumber = $SshPortNumber
    }
    $sessionKey = ("{0}:{1}" -f $sessionOptions.HostName, $portNumber)
 
    # Load known hosts (if any)
    if (Test-Path $KnownHostsFile)
    {
        [xml]$knownHosts = Get-Content $KnownHostsFile
    }
    else
    {
        $knownHosts = New-Object System.XML.XmlDocument
        $knownHosts.AppendChild($knownHosts.CreateElement("KnownHosts")) | Out-Null
    }
 
    # Lookup host key for this session 
    $fingerprint =
        $knownHosts.DocumentElement |
        Select-Xml -XPath "KnownHost[@host='$sessionKey']/@fingerprint"
 
    if ($fingerprint)
    {
        Write-Host "Connecting to a known host"
    }
    else
    {
        # Host is not known yet. Scan its host key and let the user decide.
        $session = New-Object WinSCP.Session
        try
        {
            $fingerprint = $session.ScanFingerprint($sessionOptions, "SHA-256")
        }
        finally
        {
            $session.Dispose()
        }
 
        Write-Host -NoNewline (
            "Continue connecting to an unknown server and add its host key to a cache?`n" +
            "The server's host key was not found in the cache.`n" + 
            "You have no guarantee that the server is the computer you think it is.`n" +
            "`n" +
            "The server's key fingerprint is:`n" +
            $fingerprint + "`n" +
            "`n" +
            "If you trust this host, press Y. To abandon the connection, press N. ")
 
        do
        {
            $key = [char]::ToUpperInvariant([System.Console]::ReadKey($True).KeyChar)
            if ($key -eq "N")
            {
                Write-Host($key)
                exit 2
            }
        }
        while ($key -ne "Y")
 
        Write-Host($key)
 
        # Cache the host key
        $knownHost = $knownHosts.CreateElement("KnownHost")
        $knownHosts.DocumentElement.AppendChild($knownHost) | Out-Null
        $knownHost.SetAttribute("host", $sessionKey)
        $knownHost.SetAttribute("fingerprint", $fingerprint)
 
        $knownHosts.Save($KnownHostsFile)
    }
 
    # Now we have the fingerprint
    $sessionOptions.SshHostKeyFingerprint = $fingerprint
 
    $session = New-Object WinSCP.Session
 
    try
    {
        # Connect
        $session.Open($sessionOptions)
 
        # Your code
    }
    finally
    {
        # Disconnect, clean up
        $session.Dispose()
    }
}
catch
{
    Write-Host "Error: $($_.Exception.Message)"
    exit 1
}

Advertisement

C#

using System;
using System.IO;
using System.Xml;
using WinSCP;
 
class Example
{
    const string KnownHostsFile = "KnownHosts.xml";
    const int SshPortNumber = 22;
 
    static int Main(string[] args)
    {
        try
        {
            // Setup session options
            SessionOptions sessionOptions = new SessionOptions
            {
                Protocol = Protocol.Sftp,
                HostName = "example.com",
                UserName = "user",
                Password = "mypassword",
            };
 
            // Cache key is hostname:portnumber
            int portNumber =
                (sessionOptions.PortNumber != 0) ? sessionOptions.PortNumber : SshPortNumber;
            string sessionKey = string.Format("{0}:{1}", sessionOptions.HostName, portNumber);
 
            // Load known hosts (if any)
            XmlDocument knownHosts = new XmlDocument();
            if (File.Exists(KnownHostsFile))
            {
                knownHosts.Load(KnownHostsFile);
            }
            else
            {
                knownHosts.AppendChild(knownHosts.CreateElement("KnownHosts"));
            }
 
            // Lookup host key for this session 
            XmlNode fingerprintNode =
                knownHosts.DocumentElement.SelectSingleNode(
                    "KnownHost[@host='" + sessionKey + "']/@fingerprint");
 
            string fingerprint = null;
            if (fingerprintNode != null)
            {
                fingerprint = fingerprintNode.Value;
                Console.WriteLine("Connecting to a known host");
            }
            else
            {
                // Host is not known yet. Scan its host key and let the user decide.
                using (Session session = new Session())
                {
                    fingerprint = session.ScanFingerprint(sessionOptions, "SHA-256");
                }
 
                Console.Write(
                    "Continue connecting to an unknown server and add its host key to a cache?" +
                        Environment.NewLine +
                    "The server's host key was not found in the cache." + Environment.NewLine +
                    "You have no guarantee that the server is the computer you think it is." +
                        Environment.NewLine +
                    Environment.NewLine +
                    "The server's key fingerprint is:" + Environment.NewLine +
                    fingerprint + Environment.NewLine +
                    Environment.NewLine +
                    "If you trust this host, press Y. To abandon the connection, press N. ");
 
                char key;
                do
                {
                    key = char.ToUpperInvariant(Console.ReadKey(true).KeyChar);
                    if (key == 'N')
                    {
                        Console.WriteLine(key);
                        return 2;
                    }
                }
                while (key != 'Y');
 
                Console.WriteLine(key);
 
                // Cache the host key
                XmlElement knownHost = knownHosts.CreateElement("KnownHost");
                knownHosts.DocumentElement.AppendChild(knownHost);
                knownHost.SetAttribute("host", sessionKey);
                knownHost.SetAttribute("fingerprint", fingerprint);
 
                knownHosts.Save(KnownHostsFile);
            }
 
            // Now we have the fingerprint
            sessionOptions.SshHostKeyFingerprint = fingerprint;
 
            using (Session session = new Session())
            {
                // Connect
                session.Open(sessionOptions);
 
                // Your code
            }
 
            return 0;
        }
        catch (Exception e)
        {
            Console.WriteLine("Error: {0}", e);
            return 1;
        }
    }
}

Advertisement

Last modified: by martin