Validate SAML assertion signature

Suppose you receive a SOAP message with a SAML assertion to validate, something like this:

<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:urn="urn:ihe:iti:xds-b:2007">
    <soap:Header xmlns:wsse="http://docs.oasisopen.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd">
        <wsse:Security>
            <saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Version="2.0" IssueInstant="2013-11-29T14:14:01Z" ID="assertion_1.2.3.4.5_msgId_-db207fd:142a4340b1d:-7ffe">
                <saml:Issuer>https://some.issuer.com</saml:Issuer>
                <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
                    <ds:SignedInfo>
                        <ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                        <ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/>
                        <ds:Reference URI="#assertion_1.2.3.4.5_msgId_-db207fd:142a4340b1d:-7ffe">
                            <ds:Transforms>
                                <ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/>
                                <ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#"/>
                            </ds:Transforms>
                            <ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
                            <ds:DigestValue>A7juD9LE9MWI4BPVyuvcVj2G5w4=</ds:DigestValue>
                        </ds:Reference>
                    </ds:SignedInfo>                    <ds:SignatureValue>ckZHJBiomihVKQ+9so5vJg3x2I25p4q6u/I3aQ4HndS/1G3KDmSHwVvPkp9Vyp2HVHXD86PYoDdfLhpDdAPU8MaPiT8RTShWnuOEJB9s2k1ZhMS4/OF388irKOB+cWk3TByKU8aNuFgVtwIGMwPX7uNSkBFtX0RoFwK4RbeX1sw=</ds:SignatureValue>
                    <ds:KeyInfo>
                        <ds:X509Data>                            <ds:X509Certificate>MIICiTCCAfKgAwIBAgIBATANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJVDEZMBcGA1UEChMQQ01TIE9SR0FOSVpBVElPTjEaMBgGA1UECxMRQ01TIFdvcmtpbmcgR3JvdXAxFDASBgNVBAMTC0NNUyBUZXN0IENBMB4XDTEzMTAxMDA4MzczN1oXDTE2MDkyNDA4MzczN1owVjEWMBQGA1UEAxMNQml0NElEIC0gRGVtbzEWMBQGA1UECxMNQml0NElEIC0gRGVtbzEXMBUGA1UEChMOUmVnaW9uZSBWZW5ldG8xCzAJBgNVBAYTAklUMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC0aXW6lgfGpR6p7IlAbdVFEeMAzNuf8U4FrBki+S7nHBFcpH7nMu2ePKC+UG6SZK19aagieFse9f8yomJ5tjVP7cX8w3wJfOl48rzIwX6cy3g/EA/+wrWLnY/4c2qToeME3E+QJG/J0ALo5cwc/H7hvDO6D3ReythyiGT1eTs8kwIDAQABo2MwYTAMBgNVHRMBAf8EAjAAMB8GA1UdJQQYMBYGCCsGAQUFBwMDBgorBgEEAYI3AgEWMB0GA1UdBAQWMBQwDjAMBgorBgEEAYI3AgEWAwIHgDARBglghkgBhvhCAQEEBAMCBBAwDQYJKoZIhvcNAQEFBQADgYEAC39DmsfcHGkiG+buf+ZjoVoFMSxW9YTuOlEMOg4slmzha4p7PbVctT5cncjclyLqQlJQgSWxYnv0D7AYOgxgYpaugct7ljs/9Eew0cNpLd3ofMXwIPfeIzOIaAdzhcnANEXM7ETRhH5pu3KhSo718SaP7sKLP1AlcbdSRX/OUbM=</ds:X509Certificate>
                        </ds:X509Data>
                        <ds:KeyValue>
                            <ds:RSAKeyValue>                                <ds:Modulus>tGl1upYHxqUeqeyJQG3VRRHjAMzbn/FOBawZIvku5xwRXKR+5zLtnjygvlBukmStfWmoInhbHvX/MqJiebY1T+3F/MN8CXzpePK8yMF+nMt4PxAP/sK1i52P+HNqk6HjBNxPkCRvydAC6OXMHPx+4bwzug90XsrYcohk9Xk7PJM=</ds:Modulus>
                                <ds:Exponent>AQAB</ds:Exponent>
                            </ds:RSAKeyValue>
                        </ds:KeyValue>
                    </ds:KeyInfo>
                </ds:Signature>
                <saml:Subject>
                    <saml:NameID>LRTRNS56B54H123B</saml:NameID>
                </saml:Subject>
                <saml:Conditions NotBefore="2013-11-29T14:14:01Z" NotOnOrAfter="2013-11-29T15:14:01Z">
                    <saml:AudienceRestriction>
                        <saml:Audience>http://X-ServiceProvider</saml:Audience>
                    </saml:AudienceRestriction>
                </saml:Conditions>
                <saml:AuthnStatement AuthnInstant="2013-11-29T14:14:01Z">
                    <saml:AuthnContext>
                        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocolPassword</saml:AuthnContextClassRef>
                        <saml:AuthenticatingAuthority>https://access.bit4id.org:8006</saml:AuthenticatingAuthority>
                    </saml:AuthnContext>
                </saml:AuthnStatement>
            </saml:Assertion>
        </wsse:Security>
    </soap:Header>
    <soap:Body>
        ...
    </soap:Body>
</soap:Envelope>

Install the OpenSAML libraries:

<dependency>
    <groupId>org.opensaml</groupId>
    <artifactId>opensaml</artifactId>
    <version>2.6.0</version>
</dependency>

First you must extract the assertion from the raw request message. The OpenSAML libraries provides APIs to unmarshall an Assertion object from a XML. The following snippet returns an Assertion object from the input stream of a HttpRequest:

public static Assertion inputStreamToSamlAssertion(InputStream inputStream) {
        Assertion assertion = null;
        try {
            DefaultBootstrap.bootstrap();
            DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
            documentBuilderFactory.setNamespaceAware(true);
            DocumentBuilder docBuilder = documentBuilderFactory.newDocumentBuilder();
    
            Document document = docBuilder.parse(inputStream);
            Element element = document.getDocumentElement();
            element = (Element)element.getElementsByTagNameNS("saml", "Assertion").item(0);
            
            UnmarshallerFactory unmarshallerFactory = Configuration.getUnmarshallerFactory();
            Unmarshaller unmarshaller = unmarshallerFactory.getUnmarshaller(element);
            assertion = (Assertion) unmarshaller.unmarshall(element);
        } catch (Exception ex) {
            throw new InvalidAssertionException("No assertion was found in document.");
        }
        return assertion;
    }

From the Assertion object you can extract the signature with the getSignature() method and send it to this code snippet:

public static void validateAssertionSignature(Signature signature, PublicKey publicKey) throws InvalidAssertionException {
        try {
            BasicX509Credential publicCredential = new BasicX509Credential();
            publicCredential.setPublicKey(publicKey);
            SignatureValidator signatureValidator = new SignatureValidator(publicCredential);
            signatureValidator.validate(signature);
        } catch (ValidationException e) {
            e.printStackTrace();
            throw new InvalidAssertionException("Assertion signature validation failed.");
        }
    }

The public key can be extracted from the signature itself (it's the ds:X509Certificate node tag) if present, or from the public certificate file. If the method throws exception, the signature is invalid.

Signature validation consists of two phases:

  1. Reference Validation: Each Reference's digest is verified by retrieving the corresponding resource and applying any transforms and then the specified digest method to it. The result is compared to the recorded DigestValue; if they do not match, validation fails.
  2. Signature Validation: The SignedInfo element is serialized using the canonicalization method specified in CanonicalizationMethod, the key data is retrieved using KeyInfo or by other means, and the signature is verified using the method specified in SignatureMethod.

More informations can be found here and here (this link provides an alternative method to check signature validation using only standard java libraries).

Remember that to ensure reference validation, the portion of referenced XML must not be modified in any way. This includes namespaces declaration and prefixes that can be modified during unmarshalling operations (for example using JAX-WS). The OpenSAML unmarshalling operations ensure that the assertion node maintains its integrity.