How to validate a SAML assertion with no KeyInfo

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.

1 3 1,719
3 REPLIES 3

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();
	}
}