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);
}
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);
}
}
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
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
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)
- jdk/jre/lib/security/local_policy.jar
- jdk/jre/lib/security/US_export_policy.jar
This post literally saved my butt. Super grateful!!
ReplyDeleteI am getting type error and to resolve this -noverify JVM arg required.
ReplyDeleteAll 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])
Will you be able to share your project?
DeleteWow, 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