Hi,
We receive a SAML assertion from an identity provider, which do no contain <ds:KeyInfo> element. As far as I can see that should be ok as the SAML2 spec does not require the use of <ds:KeyInfo> (Section 5.4.5). Nevertheless, the Validate SAML Policy fails verifying the signature in such assertions.
When I look in the message processor logs, I see that it complains about missing KeyInfo object:
-- --
SAML-Validate NIOThread@1 ERROR ValidateSAMLAssertion - ValidateSAMLAssertionExecution.verify() : Error validating signature javax.xml.crypto.dsig.XMLSignatureException: cannot find validation key
...
Caused by: javax.xml.crypto.KeySelectorException: Null KeyInfo object!
...
-- --
Is there a way to configure the policy in a way that it uses the certificate in the provided trust store to verify the signature? The trust store contains only one self-signed certificate, so we'll know that it's that specific provider who has signed the assertion.
I've tested with another provider, so I know self-signed certificate works ok with Validate SAML Policy (as long as <ds:KeyInfo> is present in the assertion). I can also verify the signature of the assertion not containing <ds:KeyInfo> using openssl; thus, I know the certificate is ok.
This is Edge Private Cloud Version 4.17.09.00.
Thanks!
-inan.
I don't know how to do this today, without writing your own Java callout.
Thanks for the answer.
Yes, it seemed like that. Nevertheless, I still wanted to check if anyone had a ready-to-use solution.
For my part, I've later on tested this module https://www.npmjs.com/package/saml20, which seems to be working.
Regards,
-inan.
We had to drop the nodejs solution and switch to a Java callout instead. This is probably a rare case that you would have to use a custom solution for SAML verification rather than the built-in policy, but, in case anyone does, below is some piece of code from our Java solution, based on opensaml, which might be of help:
public class VerifySAMLAssertion implements Execution {
// UnmarshallerFactory is thread-safe!
// See
// https://litsec.github.io/opensaml-javadoc-mirror/org/opensaml/opensaml-core/3.4.5/org/opensaml/core/xml/io/UnmarshallerFactory.html
private UnmarshallerFactory unmarshallerFactory;
// BasicParserPool is thread-safe, does secure XML processing by default!
// See
// https://wiki.shibboleth.net/confluence/display/OS30/Secure+XML+Processing+Requirements
BasicParserPool parserPool;
{
// the following hack is found at:
// https://stackoverflow.com/questions/37948303/opensaml3-resource-not-found-default-config-xml-in-osgi-container/39004323#39004323
//
// otherwise, configuration resources found in the jar are not found and
// initialization is not done properly,
// which eventually leads to NullPointerException when attempting to unmarshall
// SAML assertion in Apigee
//
// adapt TCCL
Thread thread = Thread.currentThread();
ClassLoader loader = thread.getContextClassLoader();
thread.setContextClassLoader(InitializationService.class.getClassLoader());
try {
InitializationService.initialize();
unmarshallerFactory = XMLObjectProviderRegistrySupport.getUnmarshallerFactory();
} catch (Exception e) { //
e.printStackTrace(System.out);
} finally {
// reset TCCL
thread.setContextClassLoader(loader);
}
try {
parserPool = new BasicParserPool();
if (!parserPool.isInitialized()) {
// the following configuration should be set for secure processing of SAML XML
// documents if using own
// DocumentBuilderFactory and DocumentBuilder as per recommendation found at
// https://wiki.shibboleth.net/confluence/display/OS30/Secure+XML+Processing+Requirements
/**
* parserPool.setNamespaceAware(true); parserPool.setCoalescing(true);
* parserPool.setIgnoreComments(true);
* parserPool.setExpandEntityReferences(false); Map<String, Boolean>
* builderFeatures = new HashMap<String, Boolean>(2);
* builderFeatures.put("http://javax.xml.XMLConstants/feature/secure-processing",
* Boolean.valueOf(true));
* builderFeatures.put("http://apache.org/xml/features/disallow-doctype-decl",
* Boolean.valueOf(true)); parserPool.setBuilderFeatures(builderFeatures);
**/
// "... can be used to validate SAML 2.0 since its schema is composable with
// SAML 1.0 or 1.1."
// https://wiki.shibboleth.net/confluence/display/OpenSAML/OSTwoUserManJavaValidation
Schema samlSchema = new SAMLSchemaBuilder(SAML1Version.SAML_11).getSAMLSchema();
parserPool.setSchema(samlSchema);
parserPool.initialize();
}
} catch (Exception e) {
e.printStackTrace(System.out);
}
}
public VerifySAMLAssertion2(Map<?, ?> properties) {
}
@Override
public ExecutionResult execute(MessageContext msgCtx, ExecutionContext exeCtx) {
try {
// read in the issuer's cert
String cert = (String) msgCtx.getVariable("issuer_cert");
// read in the saml assertion
String saml = (String) msgCtx.getVariable("saml_assertion");
// parse the saml assertion
Assertion assertion = getSamlAssertion(saml);
// we only validate & verify the signature here!
// other validations like expiry, recipient, audience, etc. done externally
// in a JS ...
// validate the digital signature
SAMLSignatureProfileValidator signatureProfileValidator = new SAMLSignatureProfileValidator();
signatureProfileValidator.validate(assertion.getSignature());
// verify the signature using the issuer's public key
Credential cred = new BasicCredential(getPublicKey(cert));
SignatureValidator.validate(assertion.getSignature(), cred);
} catch (Exception e) {
msgCtx.setVariable("saml_validation_error_full", e.toString());
return ExecutionResult.ABORT;
}
return ExecutionResult.SUCCESS;
}
private Assertion getSamlAssertion(String saml) throws Exception {
Reader reader = new InputStreamReader(new ByteArrayInputStream(saml.getBytes(StandardCharsets.UTF_8)));
Document doc = parserPool.parse(reader);
Element element = doc.getDocumentElement();
Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element);
XMLObject responseXmlObj = unmarshaller.unmarshall(element);
Assertion assertion = (Assertion) responseXmlObj;
return assertion;
}
private X509Certificate getCertificate(String b64CertStr) throws CertificateException {
byte[] decoded = Base64.getMimeDecoder().decode(
b64CertStr.replaceAll("-----BEGIN CERTIFICATE-----", "").replaceAll("-----END CERTIFICATE-----", ""));
X509Certificate cert = (X509Certificate) CertificateFactory.getInstance("X.509")
.generateCertificate(new ByteArrayInputStream(decoded));
return cert;
}
private PublicKey getPublicKey(String b64CertStr) throws CertificateException {
return getCertificate(b64CertStr).getPublicKey();
}
}