View Javadoc
1   /**
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements. See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership. The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License. You may obtain a copy of the License at
9    *
10   * http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied. See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.wss4j.dom.saml;
21  
22  import org.apache.wss4j.common.saml.SamlAssertionWrapper;
23  import org.apache.wss4j.common.util.SOAPUtil;
24  import org.apache.wss4j.dom.WSConstants;
25  import org.apache.wss4j.dom.common.AbstractSAMLCallbackHandler;
26  import org.apache.wss4j.dom.common.KeystoreCallbackHandler;
27  import org.apache.wss4j.dom.common.SAML1CallbackHandler;
28  import org.apache.wss4j.dom.common.SAML2CallbackHandler;
29  
30  import org.apache.wss4j.dom.engine.WSSConfig;
31  import org.apache.wss4j.dom.engine.WSSecurityEngine;
32  import org.apache.wss4j.dom.handler.RequestData;
33  import org.apache.wss4j.dom.handler.WSHandlerResult;
34  import org.apache.wss4j.common.crypto.Crypto;
35  import org.apache.wss4j.common.crypto.CryptoFactory;
36  import org.apache.wss4j.common.crypto.CryptoType;
37  import org.apache.wss4j.common.crypto.Merlin;
38  import org.apache.wss4j.common.ext.WSSecurityException;
39  import org.apache.wss4j.common.saml.SAMLCallback;
40  import org.apache.wss4j.common.saml.SAMLUtil;
41  import org.apache.wss4j.common.saml.bean.SubjectBean;
42  import org.apache.wss4j.common.saml.bean.Version;
43  import org.apache.wss4j.common.saml.builder.SAML1Constants;
44  import org.apache.wss4j.common.saml.builder.SAML2Constants;
45  import org.apache.wss4j.common.util.Loader;
46  import org.apache.wss4j.common.util.XMLUtils;
47  import org.apache.wss4j.dom.message.WSSecHeader;
48  import org.apache.wss4j.dom.message.WSSecSAMLToken;
49  
50  import org.junit.jupiter.api.Test;
51  import org.w3c.dom.Document;
52  
53  import javax.security.auth.callback.Callback;
54  import javax.security.auth.callback.CallbackHandler;
55  import javax.security.auth.callback.UnsupportedCallbackException;
56  
57  import java.io.IOException;
58  import java.io.InputStream;
59  import java.security.KeyStore;
60  
61  import org.w3c.dom.Element;
62  import org.w3c.dom.NodeList;
63  
64  import static org.junit.jupiter.api.Assertions.assertTrue;
65  import static org.junit.jupiter.api.Assertions.fail;
66  
67  /**
68   * This is a set of test-cases where the SAML Assertion is altered in some way and so
69   * we expect an exception to be thrown when processing it.
70   */
71  public class SamlNegativeTest {
72      private static final org.slf4j.Logger LOG =
73          org.slf4j.LoggerFactory.getLogger(SamlNegativeTest.class);
74      private WSSecurityEngine secEngine = new WSSecurityEngine();
75      private CallbackHandler callbackHandler = new KeystoreCallbackHandler();
76      private Crypto trustCrypto;
77      private Crypto issuerCrypto;
78      private Crypto userCrypto = CryptoFactory.getInstance("wss40.properties");
79  
80      public SamlNegativeTest() throws Exception {
81          WSSConfig config = WSSConfig.getNewInstance();
82          secEngine.setWssConfig(config);
83  
84          // Load the issuer keystore
85          issuerCrypto = new Merlin();
86          KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
87          ClassLoader loader = Loader.getClassLoader(SamlNegativeTest.class);
88          InputStream input = Merlin.loadInputStream(loader, "keys/wss40_server.jks");
89          keyStore.load(input, "security".toCharArray());
90          input.close();
91          ((Merlin)issuerCrypto).setKeyStore(keyStore);
92  
93          // Load the server truststore
94          trustCrypto = new Merlin();
95          KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
96          input = Merlin.loadInputStream(loader, "keys/wss40CA.jks");
97          trustStore.load(input, "security".toCharArray());
98          input.close();
99          ((Merlin)trustCrypto).setTrustStore(trustStore);
100     }
101 
102     /**
103      * Test that creates, sends and processes a signed SAML 2 sender-vouches
104      * authentication assertion. The assertion is altered and so the signature validation
105      * should fail.
106      */
107     @Test
108     public void testSAML2AuthnAssertionModified() throws Exception {
109         SAML2CallbackHandler callbackHandler = new SAML2CallbackHandler();
110         callbackHandler.setStatement(SAML2CallbackHandler.Statement.AUTHN);
111         callbackHandler.setConfirmationMethod(SAML2Constants.CONF_SENDER_VOUCHES);
112         callbackHandler.setIssuer("www.example.com");
113 
114         SAMLCallback samlCallback = new SAMLCallback();
115         SAMLUtil.doSAMLCallback(callbackHandler, samlCallback);
116         SamlAssertionWrapper samlAssertion = new SamlAssertionWrapper(samlCallback);
117 
118         Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG);
119         WSSecHeader secHeader = new WSSecHeader(doc);
120         secHeader.insertSecurityHeader();
121 
122         WSSecSignatureSAML wsSign = new WSSecSignatureSAML(secHeader);
123         wsSign.setKeyIdentifierType(WSConstants.BST_DIRECT_REFERENCE);
124 
125         Document signedDoc =
126             wsSign.build(
127                 null, samlAssertion, userCrypto, "wss40", "security"
128             );
129 
130         //
131         // Modify the assertion
132         //
133         Element envelope = signedDoc.getDocumentElement();
134         NodeList list = envelope.getElementsByTagNameNS(WSConstants.SAML2_NS, "Assertion");
135         Element assertionElement = (Element)list.item(0);
136         list = assertionElement.getElementsByTagNameNS(WSConstants.SAML2_NS, "Issuer");
137         Element issuer = (Element)list.item(0);
138         issuer.setTextContent("www.example2.com");
139 
140         if (LOG.isDebugEnabled()) {
141             LOG.debug("SAML 2 Authn Assertion (sender vouches):");
142             String outputString =
143                 XMLUtils.prettyDocumentToString(signedDoc);
144             LOG.debug(outputString);
145         }
146 
147         try {
148             verify(signedDoc, trustCrypto);
149             fail("Failure expected on a modified SAML Assertion");
150         } catch (WSSecurityException ex) {
151             assertTrue(ex.getErrorCode() == WSSecurityException.ErrorCode.FAILED_CHECK);
152         }
153     }
154 
155     /**
156      * Test that creates a signed SAML 1.1 Assertion using HOK, but then modifies the signature
157      * object by replacing the enveloped transform with the exclusive c14n transform.
158      * The signature validation should then fail - the enveloped transform is mandatory for
159      * a signed assertion.
160      */
161     @Test
162     public void testSAML1SignedKeyHolderSigModified() throws Exception {
163         SAML1CallbackHandler callbackHandler = new SAML1CallbackHandler();
164         callbackHandler.setStatement(SAML1CallbackHandler.Statement.AUTHN);
165         callbackHandler.setConfirmationMethod(SAML1Constants.CONF_HOLDER_KEY);
166         callbackHandler.setIssuer("www.example.com");
167 
168         SAMLCallback samlCallback = new SAMLCallback();
169         SAMLUtil.doSAMLCallback(callbackHandler, samlCallback);
170         SamlAssertionWrapper samlAssertion = new SamlAssertionWrapper(samlCallback);
171 
172         samlAssertion.signAssertion("wss40_server", "security", issuerCrypto, false);
173 
174         Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG);
175         WSSecHeader secHeader = new WSSecHeader(doc);
176         secHeader.insertSecurityHeader();
177 
178         WSSecSAMLToken wsSign = new WSSecSAMLToken(secHeader);
179 
180         Document signedDoc = wsSign.build(samlAssertion);
181 
182         //
183         // Modify the assertion
184         //
185         Element envelope = signedDoc.getDocumentElement();
186         NodeList list = envelope.getElementsByTagNameNS(WSConstants.SAML_NS, "Assertion");
187         Element assertionElement = (Element)list.item(0);
188         list = assertionElement.getElementsByTagNameNS(WSConstants.SIG_NS, "Signature");
189         Element sigElement = (Element)list.item(0);
190         list = sigElement.getElementsByTagNameNS(WSConstants.SIG_NS, "Transform");
191         Element transformElement = (Element)list.item(0);
192         transformElement.setAttributeNS(null, "Algorithm", WSConstants.C14N_EXCL_OMIT_COMMENTS);
193 
194         if (LOG.isDebugEnabled()) {
195             LOG.debug("Signed (modified) SAML message (key holder):");
196             String outputString =
197                 XMLUtils.prettyDocumentToString(signedDoc);
198             LOG.debug(outputString);
199         }
200 
201         try {
202             verify(signedDoc, trustCrypto);
203             fail("Expected failure on a modified signature");
204         } catch (WSSecurityException ex) {
205             assertTrue(ex.getErrorCode() == WSSecurityException.ErrorCode.FAILURE);
206         }
207     }
208 
209     /**
210      * Test that creates a signed SAML 2 Assertion using HOK, but then modifies the assertion.
211      * The signature verification should then fail.
212      */
213     @Test
214     public void testSAML2SignedKeyHolderKeyModified() throws Exception {
215         SAML2CallbackHandler callbackHandler = new SAML2CallbackHandler();
216         callbackHandler.setStatement(SAML2CallbackHandler.Statement.AUTHN);
217         callbackHandler.setConfirmationMethod(SAML2Constants.CONF_HOLDER_KEY);
218         callbackHandler.setIssuer("www.example.com");
219 
220         SAMLCallback samlCallback = new SAMLCallback();
221         SAMLUtil.doSAMLCallback(callbackHandler, samlCallback);
222         SamlAssertionWrapper samlAssertion = new SamlAssertionWrapper(samlCallback);
223 
224         samlAssertion.signAssertion("wss40_server", "security", issuerCrypto, false);
225 
226         Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG);
227         WSSecHeader secHeader = new WSSecHeader(doc);
228         secHeader.insertSecurityHeader();
229 
230         WSSecSAMLToken wsSign = new WSSecSAMLToken(secHeader);
231 
232         Document signedDoc = wsSign.build(samlAssertion);
233         //
234         // Modify the assertion
235         //
236         Element envelope = signedDoc.getDocumentElement();
237         NodeList list = envelope.getElementsByTagNameNS(WSConstants.SAML2_NS, "Assertion");
238         Element assertionElement = (Element)list.item(0);
239         assertionElement.setAttributeNS(null, "MinorVersion", "5");
240 
241         if (LOG.isDebugEnabled()) {
242             LOG.debug("Signed (modified) SAML message (key holder):");
243             String outputString =
244                 XMLUtils.prettyDocumentToString(signedDoc);
245             LOG.debug(outputString);
246         }
247 
248         try {
249             verify(signedDoc, trustCrypto);
250             fail("Expected failure on a modified signature");
251         } catch (WSSecurityException ex) {
252             assertTrue(ex.getErrorCode() == WSSecurityException.ErrorCode.FAILURE);
253         }
254     }
255 
256     /**
257      * Test that creates a signed SAML 1.1 authentication assertion that uses holder-of-key, but
258      * does not include a KeyInfo in the Subject, and hence will fail processing.
259      */
260     @Test
261     public void testHOKNoKeyInfo() throws Exception {
262         SAML1HOKNoKeyInfoCallbackHandler callbackHandler =
263             new SAML1HOKNoKeyInfoCallbackHandler();
264         callbackHandler.setStatement(SAML1CallbackHandler.Statement.AUTHN);
265         callbackHandler.setIssuer("www.example.com");
266 
267         SAMLCallback samlCallback = new SAMLCallback();
268         SAMLUtil.doSAMLCallback(callbackHandler, samlCallback);
269         SamlAssertionWrapper samlAssertion = new SamlAssertionWrapper(samlCallback);
270 
271         samlAssertion.signAssertion("wss40_server", "security", issuerCrypto, false);
272 
273         Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG);
274         WSSecHeader secHeader = new WSSecHeader(doc);
275         secHeader.insertSecurityHeader();
276 
277         WSSecSAMLToken wsSign = new WSSecSAMLToken(secHeader);
278         Document signedDoc = wsSign.build(samlAssertion);
279 
280         String outputString =
281             XMLUtils.prettyDocumentToString(signedDoc);
282         if (LOG.isDebugEnabled()) {
283             LOG.debug("SAML 1.1 Authn Assertion (key holder):");
284             LOG.debug(outputString);
285         }
286 
287         try {
288             verify(signedDoc, trustCrypto);
289             fail("Expected failure on a holder-of-key confirmation method with no KeyInfo");
290         } catch (WSSecurityException ex) {
291             assertTrue(ex.getErrorCode() == WSSecurityException.ErrorCode.FAILURE);
292         }
293     }
294 
295     /**
296      * Test that creates a SAML 1.1 authentication assertion that uses holder-of-key, but is
297      * not signed, and hence will fail processing.
298      */
299     @Test
300     public void testHOKNotSigned() throws Exception {
301         SAML1CallbackHandler callbackHandler = new SAML1CallbackHandler();
302         callbackHandler.setStatement(SAML1CallbackHandler.Statement.AUTHN);
303         callbackHandler.setConfirmationMethod(SAML1Constants.CONF_HOLDER_KEY);
304 
305         SAMLCallback samlCallback = new SAMLCallback();
306         SAMLUtil.doSAMLCallback(callbackHandler, samlCallback);
307 
308         SamlAssertionWrapper samlAssertion = new SamlAssertionWrapper(samlCallback);
309 
310         samlCallback.setIssuer("www.example.com");
311         samlCallback.setIssuerCrypto(issuerCrypto);
312         samlCallback.setIssuerKeyName("wss40_server");
313         samlCallback.setIssuerKeyPassword("security");
314         // saml.setSignAssertion(true);
315 
316         Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG);
317         WSSecHeader secHeader = new WSSecHeader(doc);
318         secHeader.insertSecurityHeader();
319 
320         WSSecSAMLToken wsSign = new WSSecSAMLToken(secHeader);
321         Document signedDoc = wsSign.build(samlAssertion);
322 
323         String outputString =
324             XMLUtils.prettyDocumentToString(signedDoc);
325         if (LOG.isDebugEnabled()) {
326             LOG.debug("SAML 1.1 Authn Assertion (unsigned key holder):");
327             LOG.debug(outputString);
328         }
329 
330         try {
331             verify(signedDoc, trustCrypto);
332             fail("Expected failure on an unsigned assertion with holder-of-key confirmation method");
333         } catch (WSSecurityException ex) {
334             assertTrue(ex.getMessage().contains("SAML token security failure"));
335         }
336     }
337 
338     /**
339      * Test that creates, sends and processes a signed SAML 2 authentication assertion, but it
340      * is rejected in processing as the signature on the assertion is not trusted.
341      */
342     @Test
343     public void testSAML2TrustFailure() throws Exception {
344         SAML2CallbackHandler callbackHandler = new SAML2CallbackHandler();
345         callbackHandler.setStatement(SAML2CallbackHandler.Statement.AUTHN);
346         callbackHandler.setConfirmationMethod(SAML2Constants.CONF_HOLDER_KEY);
347         callbackHandler.setIssuer("www.example.com");
348 
349         SAMLCallback samlCallback = new SAMLCallback();
350         SAMLUtil.doSAMLCallback(callbackHandler, samlCallback);
351         SamlAssertionWrapper samlAssertion = new SamlAssertionWrapper(samlCallback);
352 
353         samlAssertion.signAssertion(
354             "16c73ab6-b892-458f-abf5-2f875f74882e", "security",
355             CryptoFactory.getInstance("crypto.properties"), false
356         );
357 
358         Document doc = SOAPUtil.toSOAPPart(SOAPUtil.SAMPLE_SOAP_MSG);
359         WSSecHeader secHeader = new WSSecHeader(doc);
360         secHeader.insertSecurityHeader();
361 
362         WSSecSignatureSAML wsSign = new WSSecSignatureSAML(secHeader);
363         wsSign.setUserInfo("wss40", "security");
364         wsSign.setDigestAlgo("http://www.w3.org/2001/04/xmlenc#sha256");
365         wsSign.setSignatureAlgorithm("http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
366         wsSign.setKeyIdentifierType(WSConstants.BST_DIRECT_REFERENCE);
367 
368         Document signedDoc =
369             wsSign.build(userCrypto, samlAssertion, null, null, null);
370 
371         String outputString =
372             XMLUtils.prettyDocumentToString(signedDoc);
373         if (LOG.isDebugEnabled()) {
374             LOG.debug("Untrusted signed SAML 2 Authn Assertion (key holder):");
375             LOG.debug(outputString);
376         }
377 
378         try {
379             verify(signedDoc, trustCrypto);
380             fail ("Failure expected on an untrusted signed assertion");
381         } catch (WSSecurityException ex) {
382             assertTrue(ex.getErrorCode() == WSSecurityException.ErrorCode.FAILURE);
383         }
384     }
385 
386     /**
387      * Verifies the soap envelope
388      *
389      * @param doc
390      * @throws Exception Thrown when there is a problem in verification
391      */
392     private WSHandlerResult verify(Document doc, Crypto sigCrypto) throws Exception {
393         RequestData requestData = new RequestData();
394         requestData.setCallbackHandler(callbackHandler);
395         requestData.setDecCrypto(userCrypto);
396         requestData.setSigVerCrypto(sigCrypto);
397         requestData.setValidateSamlSubjectConfirmation(false);
398 
399         WSHandlerResult results = secEngine.processSecurityHeader(doc, requestData);
400 
401         String outputString =
402             XMLUtils.prettyDocumentToString(doc);
403         assertTrue(outputString.indexOf("counter_port_type") > 0 ? true : false);
404         return results;
405     }
406 
407     /**
408      * A CallbackHandler that creates a SAML 1.1 Authentication Assertion using holder-of-key,
409      * but does not include a KeyInfo in the Subject.
410      */
411     private static class SAML1HOKNoKeyInfoCallbackHandler extends AbstractSAMLCallbackHandler {
412 
413         public SAML1HOKNoKeyInfoCallbackHandler() throws Exception {
414             Crypto crypto = CryptoFactory.getInstance("wss40.properties");
415             CryptoType cryptoType = new CryptoType(CryptoType.TYPE.ALIAS);
416             cryptoType.setAlias("wss40");
417             certs = crypto.getX509Certificates(cryptoType);
418 
419             subjectName = "uid=joe,ou=people,ou=saml-demo,o=example.com";
420             subjectQualifier = "www.example.com";
421             confirmationMethod = SAML1Constants.CONF_HOLDER_KEY;
422         }
423 
424         public void handle(Callback[] callbacks)
425             throws IOException, UnsupportedCallbackException {
426             for (Callback callback : callbacks) {
427                 if (callback instanceof SAMLCallback) {
428                     SAMLCallback samlCallback = (SAMLCallback) callback;
429                     SubjectBean subjectBean =
430                         new SubjectBean(
431                             subjectName, subjectQualifier, confirmationMethod
432                         );
433                     createAndSetStatement(subjectBean, samlCallback);
434                     samlCallback.setSamlVersion(Version.SAML_11);
435                 } else {
436                     throw new UnsupportedCallbackException(callback, "Unrecognized Callback");
437                 }
438             }
439         }
440     }
441 
442 }