This handshake was rather easy - really. We'd take care of performing the authentication and initial authorization steps of the login process. In other words we'd ensure that the customer provides a good user name and password and that they have the product which would give them some entitlement to enter the vendor supplied web site. In pseudo code these were the steps this handshake needed to take.
- Format a plain text string with the following elements: &CustId=xxxx&TimeStamp=<Julian date/time value>. The CustId element was taken from the assertions being sent - while the time stamp was used as a random filler for the encryption process.
- Take plan text and encrypt the string using a shared certificate.
- Convert the binary encrypted data into a Base-64 string.
- Sanitize the Base-64 string so that it could be passed along as parameter to the vendor web site.
To obtain necessary information needed by the vendor (e.g. the CustId) the steps above needed to be reversed. The value after "&CustId=" could then be used to look up the customer and provide them with the tools necessary to perform the actions they wish.
Our technology stack is .NET - the vendors is Java. I haven't been in the Java space in over a decade at this point. But I was pretty certain that whatever encrypted string I could come up could easily be processed on the Java side by a developer.
Here's the code to build the handshake data.
public static string EncryptQueryString(string handshakeName, string plainStringToEncrypt) { string digitalCertificateName = CommonConfigurationManager.GetNonSsoHandshakeEncryptCert(handshakeName); X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine); StorePermission sp = new StorePermission(PermissionState.Unrestricted); sp.Flags = StorePermissionFlags.OpenStore; sp.Assert(); X509Certificate2 certX5092 = null; store.Open(OpenFlags.IncludeArchived); if (digitalCertificateName.Length > 0) { foreach (X509Certificate2 cert in store.Certificates) { if (cert.SubjectName.Name != null && cert.SubjectName.Name.Contains(digitalCertificateName)) { certX5092 = cert; break; } } if (certX5092 == null) { throw new Exception("No Certificate could be found in name " + digitalCertificateName); } } else { certX5092 = store.Certificates[0]; } string plainString = plainStringToEncrypt.Trim(); byte[] cipherbytes = Encoding.UTF8.GetBytes(plainString); RSACryptoServiceProvider rsa = (RSACryptoServiceProvider)certX5092.PublicKey.Key; byte[] cipher = rsa.Encrypt(cipherbytes, false); string cipherText = Convert.ToBase64String(cipher); return cipherText; }
So there's a great deal going on here...but essentially I'll find a certificate of the name found in the config file for this handshake interface. When found in the certificate store I'll then use it to encrypt the plain text string to an array of bytes, which will be converted to a Base-64 string. Code further up the chain will fix the Base-64 string to ensure that characters are properly changed to HTML escape characters if necessary.
I then got my initial feedback from the vendor. And it wasn't encouraging - it indicated that the vendor didn't really understand how this all worked and lacked the basic skills necessary to pull this off. I can only imagine what would have happened if we forced them into SAML 2.0 - well I can and when it does happen it won't be pretty. After a few back and forths and some hints I gave up and wrote the Java side of this for them. Which reverses the process so we can get the "CustId" from the encrypted string.
public static void main(String[] args) { // TODO Auto-generated method stub String uriString = "a%2bbO7VR6tQ4EJd5voT5gWxdUC4ELTQR5dvzDL4a8f71AQYK2TTW%2bvzbg%2fjoBqlAMrS9HfAYTWMfVuBSLY4Lk0ksWKg249cprtbxzlOFaPL19M9GQSrQLwwKNfx8MWyuiI2YZlgteM8cJBPIq5y9S2JU40jx38fEULEBTp83%2fsdylcUTSx9sGO1MWGRkW2ux2a6FiP7xcIYiqldAwnx8KRasCL3iC7jKb8oyPj9g3r7gHzs1jOLpwUahNC4%2fR9skXfQBqSDV3CuApuXCoUIKi6QNOlHp6iLjkpuz1vNAZJgQn4U0hlmZ%2bJDW3sl7v09VdPlTjQgIrlKroYNH1csBmng%3d%3d"; String uriString1 = "N7hoRdMeapijdzo4z35lcOdT%2b4x7cMk7LvkJMLClax5YCPsqcopeyGIyZKlb0NZ%2baAjEtTq%2fSgc43QvXarx9YX1GWxXmrCyZykSirZOpKdRiMp%2fswNLRsYaPyIj4UBZAmvMoDm%2bW3fXX%2bwslGJMPg10AjXGHC7O6G8%2f64yO7zCBF3j5i8HI7lEhlMtIfE7%2fn%2bm5dhwkC9ZEtEpZMOiFlqnqT2OIhzzMySzyNFk6Y1lUWrjk7S6%2bL1a7Gihr%2fYjjPX9Pt6RPQ9gCo4oFbfNjtqbPYHttEiJerVCNm6eYP4AiU%2f0c54YAA2DfRxDhuQW%2bq%2b%2flS83RahAy4JyRJVy%2b5ug%3d%3d"; String uriString2 = "opbLFa5UpBEK1wDtVNZ0j7srfqx447fMTAZThTL4Cr4xWYrzIpJv9qrAy3yKG73Lt7fUGWk7q%2foiy0f2r2ZjexI8lKvnAbrtD6URK3G1NFswY6PsH99YrsKdz2%2f2qvvbWfjlntqFrOitIS3Ndyt2PPBVmLCiWFSDy%2fxjE%2bL3XYo2VdEWAL%2fjpyzHhfxC1C84nytAINXDoECKVeU6n2zCg9%2bKAeM4keNpawRtFJXlB4nYj8sUayQy4LedfZk5JR%2bBMq5HWED6QCuGoAbZ7D9ablIMY%2bqKfZOd5zjSFN57qsM7Lgozdu5F80bxKDqrvR3C7GFK19YInxlEKy%2fpo8mmNg%3d%3d"; String unEncodeString = ""; final String PRIVATE_KEYFILE = "private_key.der"; final String CERT_FILE = "binary_cert.cer"; try { unEncodeString = unEncodeUri(uriString2); byte[] encryptedStuff = Base64.decodeBase64(unEncodeString); PrivateKey privateKey = getPrivateKey(PRIVATE_KEYFILE); InputStream inStream = new FileInputStream(CERT_FILE); CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate)cf.generateCertificate(inStream); Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", "SunJCE"); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] unenCryptedData = cipher.doFinal(encryptedStuff); System.out.println(new String(unenCryptedData)); } catch (UnsupportedEncodingException | FileNotFoundException | CertificateException | NoSuchAlgorithmException | NoSuchPaddingException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch(BadPaddingException e) { e.printStackTrace(); } catch (IllegalBlockSizeException e) { e.printStackTrace(); } catch (NoSuchProviderException e) { e.printStackTrace(); } System.out.println(unEncodeString); } private static PrivateKey getPrivateKey(String privateKeyFile) { try { RandomAccessFile raf = new RandomAccessFile(privateKeyFile, "r"); byte[] buff = new byte[(int)raf.length()]; raf.readFully(buff); raf.close(); PKCS8EncodedKeySpec kspec = new PKCS8EncodedKeySpec(buff); KeyFactory kf; kf = KeyFactory.getInstance("RSA"); return kf.generatePrivate(kspec); } catch (IOException | NoSuchAlgorithmException | InvalidKeySpecException e) { // TODO Auto-generated catch block e.printStackTrace(); } return null; } private static String unEncodeUri(String uriString) throws UnsupportedEncodingException { URLDecoder mydecoder = new URLDecoder(); return mydecoder.decode(uriString); }
There are a couple of tricky parts here. First the Cipher instance MUST be obtained with the parameter above ("RSA/ECB/PKCS1Padding"). Not doing this will cause you to get an error because of the padding that .NET adds to the encrypted bytes. Second, you must read in a private key file and obtain the private key in order to decrypt the string. The private key file can be generated from certificate provided via the tool OpenSSL. The best command I found for this is below:
openssl pkcs8 -topk8 -nocrypt -outform DER privatekeyfile.key host.pk8
Prior to decrypting and pulling out the results needed the Base-64 string must be converted to back to a non-HTML safe Base-64 string. Then it must be converted into an array of bytes from the Base-64 string. This is accomplished in the method unEncodeUri and by calling the Apache common method of Base64.decodeBase64(unEncodeString) which will do the job well. Once accomplished decrypted the string is rather easy. Finally in the line System.out.println(new String(unenCryptedData)) you can have the encrypted data displayed on the console.