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