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 java.security.MessageDigest;
23  import java.security.Principal;
24  import java.security.PublicKey;
25  import java.security.cert.Certificate;
26  import java.security.cert.X509Certificate;
27  import java.util.ArrayList;
28  import java.util.List;
29  
30  import org.apache.wss4j.common.ext.WSSecurityException;
31  import org.apache.wss4j.common.principal.WSDerivedKeyTokenPrincipal;
32  import org.apache.wss4j.common.saml.OpenSAMLUtil;
33  import org.apache.wss4j.common.saml.SAMLKeyInfo;
34  import org.apache.wss4j.common.saml.SamlAssertionWrapper;
35  import org.apache.wss4j.dom.WSConstants;
36  import org.apache.wss4j.dom.WSDataRef;
37  import org.apache.wss4j.dom.engine.WSSecurityEngineResult;
38  import org.apache.wss4j.dom.handler.WSHandlerResult;
39  import org.w3c.dom.Element;
40  
41  /**
42   * Some SAML Utility methods only for use in the DOM code.
43   */
44  public final class DOMSAMLUtil  {
45  
46      private static final org.slf4j.Logger LOG =
47          org.slf4j.LoggerFactory.getLogger(DOMSAMLUtil.class);
48  
49      private DOMSAMLUtil() {
50          // complete
51      }
52  
53      public static void validateSAMLResults(
54          WSHandlerResult handlerResults,
55          Certificate[] tlsCerts,
56          Element body
57      ) throws WSSecurityException {
58          List<WSSecurityEngineResult> samlResults = new ArrayList<>();
59          if (handlerResults.getActionResults().containsKey(WSConstants.ST_SIGNED)) {
60              samlResults.addAll(handlerResults.getActionResults().get(WSConstants.ST_SIGNED));
61          }
62          if (handlerResults.getActionResults().containsKey(WSConstants.ST_UNSIGNED)) {
63              samlResults.addAll(handlerResults.getActionResults().get(WSConstants.ST_UNSIGNED));
64          }
65  
66          if (samlResults.isEmpty()) {
67              return;
68          }
69  
70          List<WSSecurityEngineResult> signedResults = new ArrayList<>();
71          if (handlerResults.getActionResults().containsKey(WSConstants.SIGN)) {
72              signedResults.addAll(handlerResults.getActionResults().get(WSConstants.SIGN));
73          }
74          if (handlerResults.getActionResults().containsKey(WSConstants.UT_SIGN)) {
75              signedResults.addAll(handlerResults.getActionResults().get(WSConstants.UT_SIGN));
76          }
77  
78          for (WSSecurityEngineResult samlResult : samlResults) {
79              SamlAssertionWrapper assertionWrapper =
80                  (SamlAssertionWrapper)samlResult.get(WSSecurityEngineResult.TAG_SAML_ASSERTION);
81  
82              if (!checkHolderOfKey(assertionWrapper, signedResults, tlsCerts)) {
83                  LOG.warn("Assertion fails holder-of-key requirements");
84                  throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY);
85              }
86              if (!checkSenderVouches(assertionWrapper, tlsCerts, body, signedResults)) {
87                  LOG.warn("Assertion fails sender-vouches requirements");
88                  throw new WSSecurityException(WSSecurityException.ErrorCode.INVALID_SECURITY);
89              }
90          }
91  
92      }
93  
94      /**
95       * Check the holder-of-key requirements against the received assertion. The subject
96       * credential of the SAML Assertion must have been used to sign some portion of
97       * the message, thus showing proof-of-possession of the private/secret key. Alternatively,
98       * the subject credential of the SAML Assertion must match a client certificate credential
99       * when 2-way TLS is used.
100      * @param assertionWrapper the SAML Assertion wrapper object
101      * @param signedResults a list of all of the signed results
102      */
103     public static boolean checkHolderOfKey(
104         SamlAssertionWrapper assertionWrapper,
105         List<WSSecurityEngineResult> signedResults,
106         Certificate[] tlsCerts
107     ) {
108         List<String> confirmationMethods = assertionWrapper.getConfirmationMethods();
109         boolean isHolderOfKey = false;
110         for (String confirmationMethod : confirmationMethods) {
111             if (OpenSAMLUtil.isMethodHolderOfKey(confirmationMethod)) {
112                 isHolderOfKey = true;
113                 break;
114             }
115         }
116 
117         if (isHolderOfKey) {
118             if (tlsCerts == null && (signedResults == null || signedResults.isEmpty())) {
119                 return false;
120             }
121             SAMLKeyInfo subjectKeyInfo = assertionWrapper.getSubjectKeyInfo();
122             if (!compareCredentials(subjectKeyInfo, signedResults, tlsCerts)) {
123                 return false;
124             }
125         }
126         return true;
127     }
128 
129     /**
130      * Compare the credentials of the assertion to the credentials used in 2-way TLS or those
131      * used to verify signatures.
132      * Return true on a match
133      * @param subjectKeyInfo the SAMLKeyInfo object
134      * @param signedResults a list of all of the signed results
135      * @return true if the credentials of the assertion were used to verify a signature
136      */
137     public static boolean compareCredentials(
138         SAMLKeyInfo subjectKeyInfo,
139         List<WSSecurityEngineResult> signedResults,
140         Certificate[] tlsCerts
141     ) {
142         X509Certificate[] subjectCerts = subjectKeyInfo.getCerts();
143         PublicKey subjectPublicKey = subjectKeyInfo.getPublicKey();
144         byte[] subjectSecretKey = subjectKeyInfo.getSecret();
145 
146         //
147         // Try to match the TLS certs first
148         //
149         if (tlsCerts != null && tlsCerts.length > 0 && subjectCerts != null
150             && subjectCerts.length > 0 && tlsCerts[0].equals(subjectCerts[0])) {
151             return true;
152         } else if (tlsCerts != null && tlsCerts.length > 0 && subjectPublicKey != null
153             && tlsCerts[0].getPublicKey().equals(subjectPublicKey)) {
154             return true;
155         }
156 
157         if (subjectPublicKey == null && subjectCerts != null && subjectCerts.length > 0) {
158             subjectPublicKey = subjectCerts[0].getPublicKey();
159         }
160 
161         //
162         // Now try the message-level signatures
163         //
164         for (WSSecurityEngineResult signedResult : signedResults) {
165             X509Certificate[] certs =
166                 (X509Certificate[])signedResult.get(WSSecurityEngineResult.TAG_X509_CERTIFICATES);
167             PublicKey publicKey =
168                 (PublicKey)signedResult.get(WSSecurityEngineResult.TAG_PUBLIC_KEY);
169             byte[] secretKey =
170                 (byte[])signedResult.get(WSSecurityEngineResult.TAG_SECRET);
171             if (certs != null && certs.length > 0 && subjectCerts != null
172                 && subjectCerts.length > 0 && certs[0].equals(subjectCerts[0])) {
173                 return true;
174             }
175             if (publicKey != null && publicKey.equals(subjectPublicKey)) {
176                 return true;
177             }
178             if (checkSecretKey(secretKey, subjectSecretKey, signedResult)) {
179                 return true;
180             }
181         }
182         return false;
183     }
184 
185     private static boolean checkSecretKey(
186         byte[] secretKey,
187         byte[] subjectSecretKey,
188         WSSecurityEngineResult signedResult
189     ) {
190         if (secretKey != null && subjectSecretKey != null) {
191             if (MessageDigest.isEqual(secretKey, subjectSecretKey)) {
192                 return true;
193             } else {
194                 Principal principal =
195                     (Principal)signedResult.get(WSSecurityEngineResult.TAG_PRINCIPAL);
196                 if (principal instanceof WSDerivedKeyTokenPrincipal) {
197                     secretKey = ((WSDerivedKeyTokenPrincipal)principal).getSecret();
198                     if (MessageDigest.isEqual(secretKey, subjectSecretKey)) {
199                         return true;
200                     }
201                 }
202             }
203         }
204         return false;
205     }
206 
207     /**
208      * Check the sender-vouches requirements against the received assertion. The SAML
209      * Assertion and the SOAP Body must be signed by the same signature.
210      */
211     public static boolean checkSenderVouches(
212         SamlAssertionWrapper assertionWrapper,
213         Certificate[] tlsCerts,
214         Element body,
215         List<WSSecurityEngineResult> signed
216     ) {
217         //
218         // If we have a 2-way TLS connection, then we don't have to check that the
219         // assertion + SOAP body are signed
220         //
221         if (tlsCerts != null && tlsCerts.length > 0) {
222             return true;
223         }
224 
225         List<String> confirmationMethods = assertionWrapper.getConfirmationMethods();
226         boolean isSenderVouches = false;
227         for (String confirmationMethod : confirmationMethods) {
228             if (OpenSAMLUtil.isMethodSenderVouches(confirmationMethod)) {
229                 isSenderVouches = true;
230                 break;
231             }
232         }
233 
234         if (isSenderVouches) {
235             if (signed == null || signed.isEmpty()) {
236                 return false;
237             }
238             if (!checkAssertionAndBodyAreSigned(assertionWrapper, body, signed)) {
239                 return false;
240             }
241         }
242         return true;
243     }
244 
245     /**
246      * Return true if there is a signature which references the Assertion and the SOAP Body.
247      * @param assertionWrapper the SamlAssertionWrapper object
248      * @param body The SOAP body
249      * @param signed The List of signed results
250      * @return true if there is a signature which references the Assertion and the SOAP Body.
251      */
252     private static boolean checkAssertionAndBodyAreSigned(
253         SamlAssertionWrapper assertionWrapper,
254         Element body,
255         List<WSSecurityEngineResult> signed
256     ) {
257         for (WSSecurityEngineResult signedResult : signed) {
258             @SuppressWarnings("unchecked")
259             List<WSDataRef> sl =
260                 (List<WSDataRef>)signedResult.get(
261                     WSSecurityEngineResult.TAG_DATA_REF_URIS
262                 );
263             boolean assertionIsSigned = false;
264             boolean bodyIsSigned = false;
265             if (sl != null) {
266                 for (WSDataRef dataRef : sl) {
267                     Element se = dataRef.getProtectedElement();
268                     if (se == assertionWrapper.getElement()) {
269                         assertionIsSigned = true;
270                     }
271                     if (se == body) {
272                         bodyIsSigned = true;
273                     }
274                     if (assertionIsSigned && bodyIsSigned) {
275                         return true;
276                     }
277                 }
278             }
279         }
280         return false;
281     }
282 
283 
284 }