PrivateKeyAuthentication from Stream

Jul 20, 2013 at 12:24 AM
Edited Jul 20, 2013 at 12:47 AM
Hey all,

Situation:
  1. Want to connect from C# app to UNIX server using RSA PrivateKey (stored on local c: drive)with no passphrase. (works)
  2. Now I'd like to store the PrivateKey on another UNIX machine and be able to Stream / Read it in to be used by the same C# app. The reason is that i have the C# app installed on several machines, and I'd like to have them all be able to access the PrivateKey. This is especially true, since I will be changing my key every 90 days roughly. This will eliminate the need of distributing the PrivateKey too all the Windows machines everytime it changes. Can I pass in a 'path' to a URL or should I read the file using a System.Windows.Forms.WebBrowser?
So I noticed that the PrivateKeyFile class has 4 ways to use the overloaded method, the one that caught my attention: 'System.IO.Stream PrivateKey' seems like it would be what I'm going to do.

Is this the right method for what I'm trying to do? Something like: (psuedo)
private void button1_Click(object sender, EventArgs e)       
 {
            try
            {
                //CREDENTIALS
                //Works
    //string path = "C:\\Users\\Joe\\Documents\\DAR\\Keys\\joes_pk";

                //new path to web server with PK
                string path = "https://10.1.1.45/pk.php?pk=joes_pk";
                string unix_hostname = "10.1.1.10";
                int port = 22;
                string username = "unix_username";


                //Objects INIT
                PrivateKeyFile privatekey = new PrivateKeyFile(path);
                SshClient client = new SshClient(hostname, port, username, privatekey);
                client.Connect();

                //RUN command
                if (client.IsConnected)
                {
                    string cmd = "ls -la";
                    MessageBox.Show("Conected! Running " + cmd);
                    var output = client.RunCommand(cmd);
                    MessageBox.Show(output.Result.ToString() + " Will disconnect now...");
                    client.Disconnect();
                }
            }
            catch (Exception exc)
            {
                MessageBox.Show("Error on makeConnection:" + exc.Message.ToString() + ":" + exc.StackTrace.ToString());
            }
        }
Let me know what ideas yall have on the best way to make a centralized PrivateKey that I can use with SSH.NET, thanks!

SK
Jul 20, 2013 at 12:46 AM
Update, tried it, it errors out with the following:

at System.Security.Util.StringExpressionSet.CanonicalizePath(String path, Boolean needFullPath)
at System.Security.Util.StringExpressionSet.CreateListFromExpressions(String[] str, Boolean needFullPath)
at System.Security.Permissions.FileIOPermission.AddPathList(FileIOPermissionAccess access, AccessControlActions control, String[] pathListOrig, Boolean checkForDuplicates, Boolean needFullPath, Boolean copyPathList)
at System.Security.Permissions.FileIOPermission..ctor(FileIOPermissionAccess access, AccessControlActions control, String[] pathList, Boolean checkForDuplicates, Boolean needFullPath)
at System.IO.FileStream.Init(String path, FileMode mode, FileAccess access, Int32 rights, Boolean useRights, FileShare share, Int32 bufferSize, FileOptions options, SECURITY_ATTRIBUTES secAttrs, String msgPath, Boolean bFromProxy, Boolean useLongPath)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share, Int32 bufferSize, FileOptions options, String msgPath, Boolean bFromProxy)
at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share)
at System.IO.File.Open(String path, FileMode mode)
at Renci.SshNet.PrivateKeyFile..ctor(String fileName)
at RCERTPCDC.RencieTest.button1_Click(Object sender, EventArgs e) in C:\Users\Jonathon.Wright\Documents\Visual Studio 2010\Projects\MyTestProject\TP\View\RencieTest.cs:line 33

This is line 33:
PrivateKeyFile privatekey = new PrivateKeyFile(pathToKey);
Guess it doesn't work that way. I tried the url in the browser, the text of the key comes right up, so its the type of Object I'm passing which is causing the error. This is probably true since the first error is from the Rencie.SshNet namespace :
"Renci.SshNet.PrivateKeyFile..ctor(String fileName)" indicating it does want a String filename, not a Stream.

Am I passing the incorrect object type to my PrivateKey class, causing Renci.SshNet.PrivateKeyFile to error, or is Renci.SshNet.PrivateKeyFile not able to handle anything other than a file?

Let me know your thoughts. Thanks again!

SK
Jul 20, 2013 at 7:18 AM
Right, this lib just handles Filepaths/Filestreams.
Your Use-Case is... let's call it... interesting ;)

It's not the object you are passing, thats a string, nothing special about it.
But it's an URL File.Open() cannot handle.

You have to get the file/filestream/datastream on your own to your system.

E.g.: http://stackoverflow.com/questions/3460503/how-to-read-a-file-from-a-uri-using-streamreader

See Source:
https://sshnet.codeplex.com/SourceControl/latest#Renci.SshClient/Renci.SshNet/PrivateKeyFile.cs

And recommended for reading:
https://sshnet.codeplex.com/documentation
Jul 22, 2013 at 7:13 PM
Thanks for the reply da_rinkes, I took this weekend off from looking at it, my apologies for slow response.

Yes, I would agree, "interesting" is the best way to describe it =)

I'll read up on those links now and get back to you on any insights I get. Thanks again.
Jul 22, 2013 at 8:28 PM
Edited Jul 22, 2013 at 8:31 PM
Those are some good reads, I appreciate the info. StackOverflow (SOF) is pretty good too, they usually have a decent response rate to questions. I tried using the different methods listed, but all came short for the same reason. I'm assuming because they still are not set to return a 'file (path)' format, but are set to return the content of a file instead. According to the documentation, the source of SSH.NET, the 'open' method requires a local file.

So what is happening is the file is never downloaded to the client in a file format, merely strings. Using the last example, WebClient, I can 'GET' the file, and store it as a string, but I have no method to conver the string to a 'path_to_a_file'....or do I? =)

Here is an example for the last one I tried from the SOF, WebClient:
private void button1_Click(object sender, EventArgs e)
        {
            
            try
            {
                //CREDENTIALS
                var webClient = new WebClient();
                string myKey = webClient.DownloadString("https://10.1.1.45/pk.php?pk=joes_pk");
                string hostname = "10.1.1.10";
                int port = 22;
                string username = "unix_username";


                //Objects INIT
                PrivateKeyFile privatekey = new PrivateKeyFile(myKey);
                SshClient client = new SshClient(hostname, port, username, privatekey);
                client.Connect();

                            //RUN command
                if (client.IsConnected)
                {
                    string cmd = "ls -la";
                    MessageBox.Show("Conected! Running " + cmd);
                    var output = client.RunCommand(cmd);
                    MessageBox.Show(output.Result.ToString() + " Will disconnect now...");
                    client.Disconnect();
                }
                
            }
            catch (Exception exc)
            {
                MessageBox.Show("Error on connection:" + exc.Message.ToString() + ":" + exc.StackTrace.ToString());
            }
        }
So it throws this expected error:

System.ArgumentException was caught
  Message=Illegal characters in path.
  Source=mscorlib
  StackTrace:
       at System.IO.Path.CheckInvalidPathChars(String path)
       at System.IO.Path.GetFileName(String path)
       at System.IO.FileStream..ctor(String path, FileMode mode, FileAccess access, FileShare share)
       at System.IO.File.Open(String path, FileMode mode)
       at Renci.SshNet.PrivateKeyFile..ctor(String fileName)
       at RCERTPCDC.RencieTest.button1_Click(Object sender, EventArgs e) in C:\Users\~\Documents\Visual Studio 2010\Projects\Test\RencieTest.cs:line 40
  InnerException: 
Line 40 is:
PrivateKeyFile privatekey = new PrivateKeyFile(myKey);
PrivateKeyFile class requires a fileName (path), not a string which has the contents of the file. So when I am trying to instantiate the privateKey object as a PrivateKeyFile, by passing in a string, it obviously cannot.

Let me know of any thoughts that come to mind, I think I'm close. I'm wondering also if there is a way to reference a String in memory as a 'file'
Jul 22, 2013 at 11:18 PM
Edited Jul 22, 2013 at 11:23 PM
Well, no luck with memory file, I did eventually just tell the C# app to just write a tmp file to the users appDataPath, then it removes it shortly after to keep it from remaining on the box except while program is running. The other thing is I added an additional header to the webClient to check on the php side (if block $_SERVER), it only returns the key if the header name / value matches, otherwise you get a "404". This prevents people from getting the key by just browsing to the URL. Also put a certificate on the apache instance to have it run on SSL/TLS so that sniffing cannot occur to detect what the headers values should be to access the key. Lastly, locked down the apache server by IP to our subnet only. I'm open for other ideas on how to more secure this particular use-case =) Below is the code:
private void button1_Click(object sender, EventArgs e)
        {
            
            try
            {
                //Directory where the key will be written to temporarily.
                Program._tempKeyPath = Application.UserAppDataPath + "\\the_private_key";

                //Create a web client, send it additional headers for DiD, and download it to the workstation.
                var webClient = new WebClient();
                webClient.Headers.Set("app_name1", "app_key1");
                webClient.DownloadFile(""https://10.1.1.45/rkey.php", Program._tempKeyPath);

              
                //CREDENTIALS
                //string pathToKey = "C:\\Users\\~\\Documents\\DAR\\Keys\\joes_pk";
                string hostname = "10.1.1.10";
                int port = 22;
                string username = "unix_username";


                //Objects INIT
                PrivateKeyFile privatekey = new PrivateKeyFile(Program._tempKeyPath);
                SshClient client = new SshClient(hostname, port, username, privatekey);
                client.Connect();

                //RUN command
                if (client.IsConnected)
                {
                    string cmd = "ls -la";
                    MessageBox.Show("Conected! Running " + cmd);
                    var output = client.RunCommand(cmd);
                    MessageBox.Show(output.Result.ToString() + " Will disconnect now...");
                    client.Disconnect();
                }
               
            }
            catch (Exception exc)
            {
                MessageBox.Show("Error on connection:" + exc.Message.ToString() + ":" + exc.StackTrace.ToString());
            }
            finally
            {
                if (File.Exists(Program._tempKeyPath) == true)
                {
                    File.Delete(Program._tempKeyPath);
                }
            }
        }
Jul 23, 2013 at 6:01 AM
Edited Jul 23, 2013 at 6:01 AM
That should do the trick so there is no need for a temp-File.
var privatekeybytes = new MemoryStream(Encoding.Default.GetBytes(SshPrivateKeyString));
PrivateKeyFile privatekey = new PrivateKeyFile(privatekeybytes);
If you want super hyper secure. Use SecureString to store your PrivateKeyString.
Jul 23, 2013 at 7:13 PM
I read that a few times last night, and I honestly didn't understand your first comment. You said:
"That should do the trick so there is no need for a temp-File"
But...I AM using a temp-File, so I was a bit confused when you said it should do the trick. Please help... I am confused =)
Or did you mean that the code snippet you replied with would do the trick, so I would not need a temp file?
If so, NICE! But where does the variable SshPrivateKeyString come from if its not a file? That part I would need a bit help understanding.

Thanks!
Jul 23, 2013 at 7:22 PM
Yupp, I meant with that code you won't need a temp-file
var webClient = new WebClient();
string myKey = webClient.DownloadString("https://10.1.1.45/pk.php?pk=joes_pk");
var privatekeybytes = new MemoryStream(Encoding.Default.GetBytes(myKey));
PrivateKeyFile privatekey = new PrivateKeyFile(privatekeybytes);
Sorry, english is not my native language.
I'm better in speaking with code ;)
Jul 23, 2013 at 8:41 PM
That worked nicely, thanks da_rinkes!
Now no file needed, which is awesome, I don't want to have to deal with permissions, and files and possibly leaving a copy of that private key on the workstation.