Tuesday, March 27, 2018

Open SAML - Decryption of Encrypted Assertions

Recently I had a task to parse a SAML Response and extract necessary parameter to make a decision. I used OpenSaml for this task.

The parsing portion was easy. You will be receiving SAML Response in XML format. The XML might be Base64 Encoded, in which case, you will need to decode using the same Base64 to get plain XML Response.

  public String decodeBase64(String base64EncodedInput) {
    // its safe to URL Decode first
    URLCodec encoder = new URLCodec();
    String urlDecoded = encoder.decode(base64EncodedInput);
    
    byte[] bytes = Base64.getDecoder().decode(urlDecoded);
    
    return new String(bytes);
  }

    
Once we have the plain XML SAML Response, we can construct org.oopensaml2.saml.core.Response object from the following code snippet:

  public Response parse(String samlXml) throws IOException, SAXException, ParserConfigurationException {
    Response response;
    Element root;
    StringReader reader = new StringReader(samlXml);
    InputSource is = new InputSource(reader);

    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(true);
    DocumentBuilder documentBuilder = null;
    documentBuilder = factory.newDocumentBuilder();

    Document doc = documentBuilder.parse(is);
    root = doc.getDocumentElement();
    response = unmarshall(root);

    return response;
  }

  public  T unmarshall(Element element) {
    try {
      UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory();
      return (T) unmarshallerFactory.getUnmarshaller(element).unmarshall(element);
    } catch (UnmarshallingException ux) {
      throw new RuntimeException(ux);
    }
  }

      
After we have Response object, we can access parameters which are normally stored as Assertions. An Assertion in turn contains a list of AttributeStatements. In the AttributeStatement, we can see list of Attributes, which we can use for making required decision.

  Assertion assertion = response.getAssertions().get(0);
  AttributeStatement statement = assertion.getAttributeStatements().get(0);
  Attribute attribute = statement.getAttributes().get(0);

      

Problem

The problem is, you cannot expect to get Unencrypted Assertions every time. They will be encrypted most of the times. To extract the actual data from encrypted Assertions, we first need to decrypt it.

Public and Private Key

A public certificate is required to encrypt an Asssertion and a private key is required to decypt. SAML Tool is great tool for testing with SAML Responses. There you can generate test Public and Private keys for testing.

Decrypt Encrypted Assertion

Following method can be used for the Decryption.

  private Assertion decryptEncryptedAssertion(EncryptedAssertion encryptedAssertion) throws IOException, NoSuchPaddingException,
      NoSuchAlgorithmException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException,
      DecryptionException, InvalidKeySpecException, CertificateException {
      File privateKeyFile = new File(FORMATTED_PRIVATE_KEY_FILE_PATH);
      InputStream privateKeyStream = new FileInputStream(privateKeyFile);

      PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(IOUtils.toByteArray(privateKeyStream));
      KeyFactory factory = KeyFactory.getInstance("RSA"); // Algorithm as "RSA" here can differ based on your actual encryption
      PrivateKey privateKey = factory.generatePrivate(spec);
      BasicX509Credential cred = new BasicX509Credential();
      cred.setPrivateKey(privateKey);

      StaticKeyInfoCredentialResolver resolver = new StaticKeyInfoCredentialResolver(cred);
      Decrypter decrypter = new Decrypter(null, resolver, new InlineEncryptedKeyResolver());
      decrypter.setRootInNewDocument(true);
      Assertion decrypted = decrypter.decrypt(encryptedAssertion);
      return decrypted;
    } // decryptEncryptedAssertion

      

Issue 1 : Formatted Private Key File

In the above code snippet, if you set the path FORMATTED_PRIVATE_KEY_FILE_PATH to point to the actual private key file you have generated, then you will have following exception:
java.security.InvalidKeyException: invalid key format

Solution

The problem is, you cannot directly use the private key you have generated. You need to convert it into a Java readable format and can be done using the following Commandlint Command:

  openssl pkcs8 -topk8 -inform PEM -outform DER -in <PRIVATE_KEY_FILE_NAME> -nocrypt > pkcs8_key
      
Then FORMATTED_PRIVATE_KEY_FILE_PATH should point to this generated file pkcs8_key

Issue 2 : saml:Assertion is not bound

At some time I also had a issue where the encrypted assertion could not be decrypted. Upone debugging, I found the following message:
invalid xml, prefix saml in saml:Assertion is not bound
This was a minor error because of namespace for the prefix saml. This was fixed using a proper namespace declaration in the element saml:Assertion before encryption.
<saml:Assertion xmlns="urn:oasis:names:tc:SAML:2.0:assertion"> .... </saml:Assertion>

Issue 3: Issue During JUnit Test Using Mockito

This issue was particularly nasty one. Nasty because it was hard for me to pin point the real cause (Although it turned out to be very simple.) So my implementation was working fine when running in server mode. I fell into problem when I wrote JUnit test for it. I wrote JUnit test using Mockito. I got the following exception when running the test.

  ERROR [main] encryption.Decrypter - Failed to decrypt EncryptedKey, valid decryption key could not be resolved
  [2018-03-02 15:05:16,478] ERROR [main] encryption.Decrypter - Failed to decrypt EncryptedData using either EncryptedData KeyInfoCredentialResolver or EncryptedKeyResolver + EncryptedKey KeyInfoCredentialResolver
  [2018-03-02 15:05:16,482] ERROR [main] encryption.Decrypter - SAML Decrypter encountered an error decrypting element content
  org.opensaml.xml.encryption.DecryptionException: Failed to decrypt EncryptedData

      
The exception message valid decryption key could not be resolved was misleading the key was working in server mode and was placed in proper location.

I had to debug deep down into the library class itself to finally find the following:

 java.lang.ClassCastException: com.sun.crypto.provider.AESCipher cannot be cast to javax.crypto.CipherSpi 
 com.sun.crypto.provider.RSACipher cannot be cast to javax.crypto.CipherSpi

      

Solution

This exception message led me to HERE and found the solution was to include the following in the test class.

  @PowerMockIgnore({"com.sun.net.ssl.internal.ssl.Provider", "javax.crypto.*"})

      
Issue 4: Issue due to AES256 and AES128 Compatibility
I encountered this error after long time the solution was deployed. What happened was, all our PCs joined company network, so that we could log into our PCs through company credentials. So new profiles were created. When I tried to run this already deployed, it didn't run. Actually a JUnit test failed (Thanks to JUnit test that I encountered this error). The JUnit test was showing an error message org.opensaml.xml.encryption.DecryptionException: Failed to decrypt EncryptedData. My initial idea was, during the import of the project, the test private key file might have changed. I was completely wrong. I went on to run the whole server and tried to log in using a sample response. It failed!! Then I checked log files where I found this error message.

        org.opensaml.xml.validation.ValidationException: Unable to evaluate key against signature
 at org.opensaml.xml.signature.SignatureValidator.validate(SignatureValidator.java:74)
 at com.worldlingo.servlet.SamlServlet.verifySignature(SamlServlet.java:447)
 at com.worldlingo.servlet.SamlServlet.validateResponse(SamlServlet.java:315)
        ...........
        ...........

        Caused by: org.apache.xml.security.signature.XMLSignatureException: Signature length not correct: got 256 but was expecting 128
        Original Exception was java.security.SignatureException: Signature length not correct: got 256 but was expecting 128
 at org.apache.xml.security.algorithms.implementations.SignatureBaseRSA.engineVerify(SignatureBaseRSA.java:93)
 at org.apache.xml.security.algorithms.SignatureAlgorithm.verify(SignatureAlgorithm.java:301)
 at org.apache.xml.security.signature.XMLSignature.checkSignatureValue(XMLSignature.java:723)
 at org.opensaml.xml.signature.SignatureValidator.validate(SignatureValidator.java:69)

      
This exception reminded me of JVM jar file changes that I had made during the development. By default Java can handle only upto AES128. In order to handle AES256, I had to replace,
  1. jdk/jre/lib/security/local_policy.jar
  2. jdk/jre/lib/security/US_export_policy.jar
From the site http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html






4 comments:

  1. This post literally saved my butt. Super grateful!!

    ReplyDelete
  2. I am getting type error and to resolve this -noverify JVM arg required.
    All the apps test cases I am running through jenkin & I want to add this JVM arg specifically for one of Test cases. Is there any way to do this?

    java.lang.VerifyError: Inconsistent stackmap frames at branch target 57
    Exception Details:
    Location:
    org/opensaml/ws/soap/client/http/TLSProtocolSocketFactory.createSocket(Ljava/lang/String;ILjava/net/InetAddress;ILorg/apache/commons/httpclient/params/HttpConnectionParams;)Ljava/net/Socket; @57: aload
    Reason:
    Type 'javax/net/ssl/SSLSocketFactory' (current frame, locals[7]) is not assignable to 'javax/net/SocketFactory' (stack map, locals[7])
    Current Frame:
    bci: @33
    flags: { }
    locals: { 'org/opensaml/ws/soap/client/http/TLSProtocolSocketFactory', 'java/lang/String', integer, 'java/net/InetAddress', integer, 'org/apache/commons/httpclient/params/HttpConnectionParams', integer, 'javax/net/ssl/SSLSocketFactory' }
    stack: { integer }
    Stackmap Frame:
    bci: @57
    flags: { }
    locals: { 'org/opensaml/ws/soap/client/http/TLSProtocolSocketFactory', 'java/lang/String', integer, 'java/net/InetAddress', integer, 'org/apache/commons/httpclient/params/HttpConnectionParams', integer, 'javax/net/SocketFactory' }
    stack: { }
    Bytecode:
    0x0000000: 1905 c700 0dbb 000f 5912 17b7 0018 bf19
    0x0000010: 05b6 0019 3606 2ab4 000c b600 123a 0715
    0x0000020: 069a 0018 1907 2b1c 2d15 04b6 001a 3a08
    0x0000030: 2a19 08b6 0014 1908 b019 07b6 001b 3a08
    0x0000040: bb00 1c59 2d15 04b7 001d 3a09 bb00 1c59
    0x0000050: 2b1c b700 1e3a 0a19 0819 09b6 001f 1908
    0x0000060: 190a 1506 b600 202a 1908 b600 1419 08b0
    0x0000070:
    Stackmap Table:
    same_frame(@15)
    append_frame(@57,Integer,Object[#121])

    ReplyDelete
  3. Wow, What an Outstanding post. I found this too much informatics. It is what I was seeking for. I would like to recommend you that please keep sharing such type of info.If possible, Thanks. Ultimate Encrypted Phone Solutions Chat PGP

    ReplyDelete