Click here to Skip to main content
15,867,453 members
Articles / Security

RSA Private Key Import from PEM Format in C#

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
14 Jun 2011Ms-PL6 min read 98.6K   11   5
How to import OpenSSL private key into .NET application and use it with X509 public certificate to establish TLS connection with asymmetric encryption and two phase certificates handshake

First of all, I want to apologize for not writing. On the one hand, this is not a good thing for me to disappear from the development community horizons, on the other hand, I am investing all my time into our better feature, which is a good thing. Too many things were done during the last two years. And the good news is that we already delivered whatever was promised and know for sure that we are able to deliver even more in the future. But let’s get to business. First of all, I have a huge pipeline of interesting articles to share with you, second, some people from my team have also decided to contribute to the community and write Better Place development team blog. There are not too many there, but that is only a matter of time.

Today, we’ll speak about security. About how to import OpenSSL private key into .NET application and use it along with X509 public certificate to establish TLS connection with asymmetric encryption and two phase certificates handshake.

image

Let’s start from the very beginning. What is SSL? SSL is the secure way to communicate when transferred data is encrypted by using one time and per-session cipher. There are different implementations of such connection. The most famous one is the one all of you use when connecting to https://something… When doing this, your browser asks the remote side to provide its public certificate for you in order to check it with local “authority” you trusted in. If everything is ok and the host defined on the remote certificate is the host you are speaking with, your browser allows communication after both sides decide about the one-time cipher for encryption.

You can implement this mode of SSL very easily by using SslStream class in .NET as 1-2-3.

  1. Resolve host and open TcpClient connection to it:
    C#
    var host = new IPHostEntry();     
    
    try {      
        host = Dns.GetHostEntry(RemoteAddress.DnsSafeHost);      
    } catch (SocketException soe) {      
        if (soe.SocketErrorCode == SocketError.HostNotFound) {      
              host.HostName = RemoteAddress.DnsSafeHost;      
        }      
    }
    
    Client.Connect(host.HostName, RemoteAddress.Port); 
  2. Initialize SSL encrypted stream to it by providing validation callback for remote certificate:
    C#
    var stream = new SslStream(Client.GetStream(), true, _validateCertificate);
  3. Ask for authorization:
    C#
    stream.AuthenticateAsClient(host.HostName);

    Inside remote certificate validation callback, you should decide what to do if something bad happened during the negotiation phase.

C#
private readonly RemoteCertificateValidationCallback _validateCertificate = 
    (sender, certificate, chain, sslPolicyErrors) => {     
  var result = sslPolicyErrors == SslPolicyErrors.None;      
    if (!result) {      
      var err = new StringBuilder();      
        err.AppendFormat("Unable to establish security connection due to {0}. 
			Error chain:",
        sslPolicyErrors);        

        foreach (var el in chain.ChainElements) {     
         foreach (var s in el.ChainElementStatus) {      
            err.AppendFormat("{0} – {1}", el.Certificate.Subject, s.StatusInformation);
           }      
         }      
        Log.Warn(err.ToString());      
       }      
      return result;      
    };

So far, so good. Now, if everything is OK, just use SslStream as a regular stream to write and read from the socket. All other complicated things will be done by .NET.

However this is only a part of the game. Now comes the real thing. What if you want to be more secure and want your server to be able to validate that the local client is one that it can trust. This scenario is often used in closed networks, when server side (or any other provisioning entity) can assure that every client is well known and that it is able to provide certificate to each of those. For this scenario, we also have solution in SslStream implementation, which takes into account this ability, defined by TLS RFC. All we need is to use other override of SslStream constructor which receives the callback for client certificate, choose logic and authorization method with prepared clients certificates.

C#
var stream = new SslStream(Client.GetStream(), 
		true, _validateCertificate, _selectCertificate);     
stream.AuthenticateAsClient(host.HostName, _clientCerts, SslProtocols.Ssl3, false);

Inside local certificate selection logic, you should receive the remote end choice algorithm and return the most secure client certificate you have.

C#
private readonly LocalCertificateSelectionCallback _selectCertificate = 
    (sender, target, localCerts, remoteCert, issuers) => {     
    ….      
return securestCert;      
}

Also you should prepare the local certificates collection, provided as input to negotiation method. This one is simple too. All you need is a standard X509 certificate(s). Usually, such certificates are provided by uber-secure-unix-seriose-unbreakable-machine, which use OpenSSL to export generated keys. This means that in most cases, your public certificate will look inside like this:

Certificate:     
    Data:      
        Version: 1 (0×0)      
        Serial Number: 268436473 (0x100003f9)      
        Signature Algorithm: md5WithRSAEncryption      
        Issuer: O=UBER, OU=RD/emailAddress=ca@ubersecurity.org, 
        L=TLV, ST=Israel, C=IL, CN=ca      
        Validity      
            Not Before: May 25 11:26:50 2011 GMT      
            Not After : May 24 11:26:50 2012 GMT      
        Subject: C=IL, ST=Israel, O=UBER, OU=SEC, CN=UberSecurity      
        Subject Public Key Info:      
            Public Key Algorithm: rsaEncryption      
            RSA Public Key: (1024 bit)      
                Modulus (1024 bit):      
… some random HEX numbers …      
Exponent: 65537 (0×10001)      
    Signature Algorithm: md5WithRSAEncryption      
        … some other random HEX numbers …      
—–BEGIN CERTIFICATE—–      
… some BASE64 random characters here …      
—–END CERTIFICATE—– 

This format is called PEM (Privacy Enhanced Mail). This is the most common and easiest format for secure text transfer. Such file can be easily imported and used by X509Certificate class as follows:

C#
var clientCert = X509Certificate.CreateFromCertFile("myCert.pem");

That’s all. All you need now is to add this certificate into certificate collection (_clientCerts in this case) and return it when _selectCertificate delegate is being called.

Looks simple and secure? It is, but there is a small BUT in all this. Real security experts, come from OpenSSL world often do not want to put private key for client (the key will be used for outgoing traffic encryption) inside client certificate and want to provide it via other channel securely.

Now you are asking what I am speaking about? Let me explain:

When SSL uses asymmetric encryption algorithm, local side uses private key to encrypt outgoing traffic. Once it trusts the other side (by validating remote certificate), it sends local public key to the remote side, which uses it for information decryption. So far, we have three entities: public key, private key and certificate. There is a method commonly used by the industry to minimize transit problems. We know to pack public certificate and wrapped public key inside the same store to send it. If we want to go even further, we can also store securely private key inside the same store. Looks not very secure? This is not quite right. First of all, in most cases private certificate is encrypted by using special keyphase only known to the side this certificate intended to, second, it uses the same public key + certificate itself hash values to encrypt it event better. In this case, there is a big advantage of compact and well known package format (keypair + certificate) and high security level.

However, people coming from the OpenSSL world not trust too much in this method (and called it “evil empire bought the patent”) and often provide encrypted private key separately. This key is being transferred in PEM format, however this time it is not the standard one, but specific and designed by OpenSSL geeks. Even if they call it RSA format, it has almost no relation to it.

Such key looks as follows:

—–BEGIN RSA PRIVATE KEY—–     
Proc-Type: 4,ENCRYPTED      
DEK-Info: DES-EDE3-CBC,…some geeky HEX here …

… some BASE64 random characters here …    

—–END RSA PRIVATE KEY—–

Looks simple? Do not hurry this much. .NET has no built in method to read this format. So we’ll have to write one, based on OpenSSL specification. Let’s start:

First of all, “well known headers”:

C#
private const string _begin = "—–BEGIN ";     
private const string _end = "—–END ";      
private const string _private = "PRIVATE KEY";      
private const string _public = "PUBLIC KEY";      
private const string _rsaPublic = "RSA PUBLIC KEY";      

Next read the text inside the file:

C#
using (var reader = new StringReader(data)) {     
   var line = reader.ReadLine();      
   if (line.NotNull() && line.StartsWith(_begin)) {      
      line = line.Substring(_begin.Length);      
      var idx = line.IndexOf(‘-’);      
      if (idx > 0) {      
         var type = line.Before(idx);      
         return _loadPem(reader, type, passKey);      
      }      
   }      
   throw new ArgumentException("This is not valid PEM format", "data", 
        new FormatException("PEM start identifier is invalid or not found."));      
}      

…and read headers:

C#
var end = _end + type;     
var headers = new _pemHeaders();      
var line = string.Empty;      
var body = new StringBuilder();      
while ((line = reader.ReadLine()) != null && line.IndexOf(end) == -1) {      
   if (line == null) {      
      throw new FormatException("PEM end identifier is invalid or not found.");      
   }      
   var d = line.IndexOf(‘:’);      
   if (d >= 0) {      
      // header      
   
      var n = line.Substring(0, d).Trim();      
      if (n.StartsWith("X-")) n = n.Substring(2);      
      var v = line.After(d).Trim();      
      if (!headers.ContainsKey(n)) {      
         headers.Add(n, v);      
      } else {      
         throw new FormatException("Duplicate header {0} in PEM data.".Substitute(n));
      }

When headers are ready, we need to read a body. This is base64 encrypted:

C#
   } else {      
      // body      
      body.Append(line);      
   }      
}      
if (body.Length % 4 != 0 || type.EndsWith(_private)) {      
   throw new FormatException("PEM data is invalid or truncated.");      
}

return _createPem(type, headers, 
Convert.FromBase64String(body.ToString()), passkey);

And now, based on headers, we can decode body. For simplification, we’ll decode only most common encryptions for the key.

C#
type = type.Before(type.Length – _private.Length).Trim();     
var pType = headers.TryGet("Proc-Type");      
if (pType == "4,ENCRYPTED") {      
   if (passkey.IsEmpty()) {      
      throw new ArgumentException
	("Passkey is mandatory for encrypted PEM object");      
   } 

   var dek = headers.TryGet("DEK-Info");     
   var tkz = dek.Split(‘,’);      
   if (tkz.Length > 1) {      
      var alg = new _alg(tkz[0]);      
      var saltLen = tkz[1].Length;      
      var salt = new byte[saltLen / 2];      
      for (var i = 0; i < saltLen / 2; i++) {      
         var pair = tkz[1].Substring(2 * i, 2);      
         salt[i] = Byte.Parse(pair, NumberStyles.AllowHexSpecifier);      
      }

      body = _decodePem(body, passkey, alg, salt);     
      if (body != null) {      
         return _decodeRsaPrivateKey(body);      
      }      
   } else {      
      throw new FormatException("DEK information is invalid or truncated.");      
   }      
}       

For simplification, we’ll support only the most common encryption algorithms (3DES with CBC mode). In general, RSA private key can be encrypted by AES, Blow Fish, DES/Triple DES and RC2.

C#
private static byte[] _decodePem(byte[] body, 
	string passkey, _alg alg, byte[] salt) {     
   if (alg.AlgBase != _alg.BaseAlg.DES_EDE3 && alg.AlgMode != _alg.Mode.CBC) {      
      throw new NotSupportedException("Only 3DES-CBC keys are supported.");      
   }      
   var des = _get3DesKey(salt, passkey);      
   if (des == null) {      
      throw new ApplicationException
	("Unable to calculate 3DES key for decryption.");      
   }      
   var rsa = _decryptRsaKey(body, des, salt);      
   if (rsa == null) {      
      throw new ApplicationException("Unable to decrypt RSA private key.");      
   }      
   return rsa;      
}      

And decrypt itself:

C#
private static byte[] _decryptRsaKey(byte[] body, byte[] desKey, byte[] iv) {     
   byte[] result = null;      
   using (var stream = new MemoryStream()) {      
      var alg = TripleDES.Create();      
      alg.Key = desKey;      
      alg.IV = iv;      
      try {      
         using (var cs = new CryptoStream(stream, alg.CreateDecryptor(), 
            CryptoStreamMode.Write)) {      
            cs.Write(body, 0, body.Length);      
            cs.Close();      
         }      
         result = stream.ToArray();      
      } catch (CryptographicException ce) {      
         // throw up      
         throw ce;      
      } catch (Exception ex) {      
         Log.Exception(ex, Severity.Info, "Failed to write crypto stream.");      
      };      
   }      
   return result;      
}      

by getting 3DES key from stream:

C#
private static byte[] _get3DesKey(byte[] salt, string passkey) {     
   var HASHLENGTH = 16;      
   var m = 2; // 2 iterations for at least 24 bytes      
   var c = 1; // 1 hash for Open SSL      
   var k = new byte[HASHLENGTH * m];

   var pk = Encoding.ASCII.GetBytes(passkey);     
   var data = new byte[salt.Length + pk.Length];      
   Array.Copy(pk, data, pk.Length);      
   Array.Copy(salt, 0, data, pk.Length, salt.Length);      
   var md5 = new MD5CryptoServiceProvider();      
   byte[] result = null;      
   var hash = new byte[HASHLENGTH + data.Length];      
   
   for (int i = 0; i < m; i++) {      
      if (i == 0) {      
         result = data;      
      } else {      
         Array.Copy(result, hash, result.Length);      
         Array.Copy(data, 0, hash, result.Length, data.Length);      
         result = hash;      
      }

      for (int j = 0; j < c; j++) {     
         result = md5.ComputeHash(result);      
      }      
      Array.Copy(result, 0, k, i * HASHLENGTH, result.Length);      
   }      
   var dk = new byte[24]; //final key      
   Array.Copy(k, dk, dk.Length);      
   return dk;      
}      

When we decode the body, we can use create RSACryptoServiceProvider class from it to be used by our SslStream. Oh, yeah, some crazy math here:

C#
using (var ms = new MemoryStream(body)) {     
   using (var reader = new BinaryReader(ms)) {      
      try {      
         var tb = reader.ReadUInt16(); // LE: x30 x81      
         if (tb == 0×8130) {      
            reader.ReadByte(); // fw 1      
         } else if (tb == 0×8230) {      
            reader.ReadInt16(); // fw 2      
         } else {      
            return null;      
         }

         tb = reader.ReadUInt16(); // version     
         if (tb != 0×0102) {      
            return null;      
         }      
         if (reader.ReadByte() != 0×00) {      
            return null;      
         }

         var MODULUS = _readInt(reader);     
         var E = _readInt(reader);      
         var D = _readInt(reader);      
         var P = _readInt(reader);      
         var Q = _readInt(reader);      
         var DP = _readInt(reader);      
         var DQ = _readInt(reader);      
         var IQ = _readInt(reader);

         var result = new RSACryptoServiceProvider();     
         var param = new RSAParameters {      
            Modulus = MODULUS,      
            Exponent = E,      
            D = D,      
            P = P,      
            Q = Q,      
            DP = DP,      
            DQ = DQ,      
            InverseQ = IQ      
         };      
         result.ImportParameters(param);      
         return result;

      } catch (Exception ex) {     
         Log.Exception(ex);      
      } finally {      
         reader.Close();      
      }      
   }      
}      

Some helper methods to read bytes and we done:

C#
private static Func<BinaryReader, byte[]> _readInt = r => {     
   var s = _getIntSize(r);      
   return r.ReadBytes(s);      
};

private static Func<BinaryReader, int> _getIntSize = r => {     
   byte lb = 0×00;      
   byte hb = 0×00;      
   int c = 0;      
   var b = r.ReadByte();      
   if (b != 0×02) { //int      
      return 0;      
   }      
   b = r.ReadByte();

   if (b == 0×81) {     
      c = r.ReadByte(); //size      
   } else      
      if (b == 0×82) {      
         hb = r.ReadByte(); //size      
         lb = r.ReadByte();      
         byte[] m = { lb, hb, 0×00, 0×00 };      
         c = BitConverter.ToInt32(m, 0);      
      } else {      
         c = b; //got size      
      }

   while (r.ReadByte() == 0×00) { //remove high zero      
      c -= 1;      
   }      
   r.BaseStream.Seek(-1, SeekOrigin.Current); // last byte is not zero, go back;      
   return c;      
};      

We are done. All we have to do now is to construct our private key and pack it for SslStream use. For this purpose, we have X509Certificate big brother X509Certificate2.

C#
var cert = new X509Certificate2(File.ReadAllBytes("myCert.pem")) {     
  PrivateKey = FromPem(Encoding.ASCII.GetString(File.ReadAllBytes("myKey.pem")), 
  _sslPrivateKeyPasskey)      
}; 

Now when you supply cert as the client certificate, SslStream will use private key for outgoing stream encryption, provide public key for remote incoming stream encryption and certificate for remote side identification.

We are done. Be good people and subscribe to our dev blog, it promised to be one of the most interesting blogs for those who is not satisfied with the way Windows works and want to pimp it a bit.

P.S. If, in case, you got an invitation from Microsoft Israel to participate in “Be what’s next” event next Wednesday 22nd. It is highly recommended to come and see me (and other large ISVs) speak about solutions we did. If you did not get an invitation, and you are a Microsoft partner, please contact local DPE guys. This is for certain ISVs and only by invitations.

image

License

This article, along with any associated source code and files, is licensed under The Microsoft Public License (Ms-PL)


Written By
Architect Better Place
Israel Israel
Hello! My name is Tamir Khason, and I am software architect, project manager, system analyst and [of course] programmer. In addition to writing big amount of documentation, I also write code, a lot of code. I used to work as a freelance architect, project manager, trainer, and consultant here, in Israel, but recently join the company with extremely persuasive idea - to make a world better place. I have very pretty wife and 3 charming kids, but unfortunately almost no time for them.

To be updated within articles, I publishing, visit my blog or subscribe RSS feed. Also you can follow me on Twitter to be up to date about my everyday life.

Comments and Discussions

 
QuestionZip points to nothing Pin
Wrangly29-Sep-20 23:54
Wrangly29-Sep-20 23:54 
QuestionZIP file not available Pin
Hans Milling29-Apr-19 22:30
Hans Milling29-Apr-19 22:30 
Questioni found the simple way to load RSA keypair from PEM format in C# Pin
pham phong15-Nov-14 5:42
pham phong15-Nov-14 5:42 
AnswerRe: i found the simple way to load RSA keypair from PEM format in C# Pin
Hans Milling29-Apr-19 22:31
Hans Milling29-Apr-19 22:31 
QuestionPasskey Pin
Paulo Pinto21-Jun-13 5:18
Paulo Pinto21-Jun-13 5:18 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.