View Javadoc

1   /*
2    * Copyright [2007] [University Corporation for Advanced Internet Development, Inc.]
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package org.opensaml.saml2.metadata.provider;
18  
19  import java.util.Iterator;
20  
21  import org.opensaml.saml2.metadata.AffiliationDescriptor;
22  import org.opensaml.saml2.metadata.EntitiesDescriptor;
23  import org.opensaml.saml2.metadata.EntityDescriptor;
24  import org.opensaml.saml2.metadata.RoleDescriptor;
25  import org.opensaml.security.SAMLSignatureProfileValidator;
26  import org.opensaml.xml.XMLObject;
27  import org.opensaml.xml.security.CriteriaSet;
28  import org.opensaml.xml.security.SecurityException;
29  import org.opensaml.xml.security.credential.UsageType;
30  import org.opensaml.xml.security.criteria.UsageCriteria;
31  import org.opensaml.xml.signature.SignableXMLObject;
32  import org.opensaml.xml.signature.Signature;
33  import org.opensaml.xml.signature.SignatureTrustEngine;
34  import org.opensaml.xml.validation.ValidationException;
35  import org.opensaml.xml.validation.Validator;
36  import org.slf4j.Logger;
37  import org.slf4j.LoggerFactory;
38  
39  /**
40   * A metadata filter that validates XML signatures.
41   */
42  public class SignatureValidationFilter implements MetadataFilter {
43      
44      /** Class logger. */
45      private final Logger log = LoggerFactory.getLogger(SignatureValidationFilter.class);
46  
47      /** Trust engine used to validate a signature. */
48      private SignatureTrustEngine signatureTrustEngine;
49  
50      /** Indicates whether signed metadata is required. */
51      private boolean requireSignature;
52      
53      /** Set of externally specified default criteria for input to the trust engine. */
54      private CriteriaSet defaultCriteria;
55      
56      /** Pre-validator for XML Signature instances. */
57      private Validator<Signature> sigValidator;
58  
59      /**
60       * Constructor.
61       * 
62       * @param engine the trust engine used to validate signatures on incoming metadata.
63       */
64      public SignatureValidationFilter(SignatureTrustEngine engine) {
65          if (engine == null) {
66              throw new IllegalArgumentException("Signature trust engine may not be null");
67          }
68  
69          signatureTrustEngine = engine;
70          sigValidator = new SAMLSignatureProfileValidator();
71      }
72      
73      /**
74       * Constructor.
75       * 
76       * @param engine the trust engine used to validate signatures on incoming metadata.
77       * @param signatureValidator optional pre-validator used to validate Signature elements prior to the actual
78       *            cryptographic validation operation
79       */
80      public SignatureValidationFilter(SignatureTrustEngine engine, Validator<Signature> signatureValidator) {
81          if (engine == null) {
82              throw new IllegalArgumentException("Signature trust engine may not be null");
83          }
84  
85          signatureTrustEngine = engine;
86          sigValidator = signatureValidator;
87      }
88  
89      /**
90       * Gets the trust engine used to validate signatures on incoming metadata.
91       * 
92       * @return trust engine used to validate signatures on incoming metadata
93       */
94      public SignatureTrustEngine getSignatureTrustEngine() {
95          return signatureTrustEngine;
96      }
97      
98      /**
99       * Get the validator used to perform pre-validation on Signature tokens.
100      * 
101      * @return the configured Signature validator, or null
102      */
103     public Validator<Signature> getSignaturePrevalidator() {
104         return sigValidator;
105     }
106 
107     /**
108      * Gets whether incoming metadata's root element is required to be signed.
109      * 
110      * @return whether incoming metadata is required to be signed
111      */
112     public boolean getRequireSignature() {
113         return requireSignature;
114     }
115 
116     /**
117      * Sets whether incoming metadata's root element is required to be signed.
118      * 
119      * @param require whether incoming metadata is required to be signed
120      */
121     public void setRequireSignature(boolean require) {
122         requireSignature = require;
123     }
124  
125     /**
126      * Get the set of default criteria used as input to the trust engine.
127      * 
128      * @return the criteria set
129      */
130     public CriteriaSet getDefaultCriteria() {
131         return defaultCriteria;
132     }
133     
134     /**
135      * Set the set of default criteria used as input to the trust engine.
136      * 
137      * @param newCriteria the new criteria set to use
138      */
139     public void setDefaultCriteria(CriteriaSet newCriteria) {
140         defaultCriteria = newCriteria;
141     }
142 
143     /** {@inheritDoc} */
144     public void doFilter(XMLObject metadata) throws FilterException {
145         SignableXMLObject signableMetadata = (SignableXMLObject) metadata;
146 
147         if (!signableMetadata.isSigned()){
148             if (getRequireSignature()) {
149                 throw new FilterException("Metadata root element was unsigned and signatures are required.");
150             }
151         }
152         
153         if (signableMetadata instanceof EntityDescriptor) {
154             processEntityDescriptor((EntityDescriptor) signableMetadata);
155         } else if (signableMetadata instanceof EntitiesDescriptor) {
156             processEntityGroup((EntitiesDescriptor) signableMetadata);
157         } else {
158             log.error("Internal error, metadata object was of an unsupported type: {}", metadata.getClass().getName());
159         }
160     }
161     
162     /**
163      * Process the signatures on the specified EntityDescriptor and any signed children.
164      * 
165      * If signature verification fails on a child, it will be removed from the entity descriptor.
166      * 
167      * @param entityDescriptor the EntityDescriptor to be processed
168      * @throws FilterException thrown if an error occurs during the signature verification process
169      *                          on the root EntityDescriptor specified
170      */
171     protected void processEntityDescriptor(EntityDescriptor entityDescriptor) throws FilterException {
172         String entityID = entityDescriptor.getEntityID();
173         log.trace("Processing EntityDescriptor: {}", entityID);
174         
175         if (entityDescriptor.isSigned()) {
176             verifySignature(entityDescriptor, entityID, false);
177         }
178         
179         Iterator<RoleDescriptor> roleIter = entityDescriptor.getRoleDescriptors().iterator();
180         while (roleIter.hasNext()) {
181            RoleDescriptor roleChild = roleIter.next(); 
182             if (!roleChild.isSigned()) {
183                 log.trace("RoleDescriptor member '{}' was not signed, skipping signature processing...",
184                         roleChild.getElementQName());
185                 continue;
186             } else {
187                 log.trace("Processing signed RoleDescriptor member: {}", roleChild.getElementQName());
188             }
189             
190             try {
191                 String roleID = getRoleIDToken(entityID, roleChild);
192                 verifySignature(roleChild, roleID, false);
193             } catch (FilterException e) {
194                log.error("RoleDescriptor '{}' subordinate to entity '{}' failed signature verification, " 
195                        + "removing from metadata provider", 
196                        roleChild.getElementQName(), entityID); 
197                roleIter.remove();
198             }
199         }
200         
201         if (entityDescriptor.getAffiliationDescriptor() != null) {
202             AffiliationDescriptor affiliationDescriptor = entityDescriptor.getAffiliationDescriptor();
203             if (!affiliationDescriptor.isSigned()) {
204                 log.trace("AffiliationDescriptor member was not signed, skipping signature processing...");
205             } else {
206                 log.trace("Processing signed AffiliationDescriptor member with owner ID: {}", 
207                         affiliationDescriptor.getOwnerID());
208                 
209                 try {
210                     verifySignature(affiliationDescriptor, affiliationDescriptor.getOwnerID(), false);
211                 } catch (FilterException e) {
212                     log.error("AffiliationDescriptor with owner ID '{}' subordinate to entity '{}' " + 
213                             "failed signature verification, removing from metadata provider", 
214                             affiliationDescriptor.getOwnerID(), entityID); 
215                     entityDescriptor.setAffiliationDescriptor(null);
216                 }
217                 
218             }
219         }
220     }
221  
222     
223     /**
224      * Process the signatures on the specified EntitiesDescriptor and any signed children.
225      * 
226      * If signature verification fails on a child, it will be removed from the entities descriptor group.
227      * 
228      * @param entitiesDescriptor the EntitiesDescriptor to be processed
229      * @throws FilterException thrown if an error occurs during the signature verification process
230      *                          on the root EntitiesDescriptor specified
231      */
232     protected void processEntityGroup(EntitiesDescriptor entitiesDescriptor) throws FilterException {
233         log.trace("Processing EntitiesDescriptor group: {}", entitiesDescriptor.getName());
234         
235         if (entitiesDescriptor.isSigned()) {
236             verifySignature(entitiesDescriptor, entitiesDescriptor.getName(), true);
237         }
238         
239         Iterator<EntityDescriptor> entityIter = entitiesDescriptor.getEntityDescriptors().iterator();
240         while (entityIter.hasNext()) {
241             EntityDescriptor entityChild = entityIter.next();
242             if (!entityChild.isSigned()) {
243                 log.trace("EntityDescriptor member '{}' was not signed, skipping signature processing...",
244                         entityChild.getEntityID());
245                 continue;
246             } else {
247                 log.trace("Processing signed EntityDescriptor member: {}", entityChild.getEntityID());
248             }
249             
250             try {
251                 processEntityDescriptor(entityChild);
252             } catch (FilterException e) {
253                log.error("EntityDescriptor '{}' failed signature verification, removing from metadata provider", 
254                        entityChild.getEntityID()); 
255                entityIter.remove();
256             }
257         }
258         
259         Iterator<EntitiesDescriptor> entitiesIter = entitiesDescriptor.getEntitiesDescriptors().iterator();
260         while(entitiesIter.hasNext()) {
261             EntitiesDescriptor entitiesChild = entitiesIter.next();
262             log.trace("Processing EntitiesDescriptor member: {}", entitiesChild.getName());
263             try {
264                 processEntityGroup(entitiesChild);
265             } catch (FilterException e) {
266                log.error("EntitiesDescriptor '{}' failed signature verification, removing from metadata provider", 
267                        entitiesChild.getName()); 
268                entitiesIter.remove();
269             }
270         }
271         
272     }
273 
274     /**
275      * Evaluate the signature on the signed metadata instance.
276      * 
277      * @param signedMetadata the metadata object whose signature is to be verified
278      * @param metadataEntryName the EntityDescriptor entityID, EntitiesDescriptor Name,
279      *                          AffiliationDescriptor affiliationOwnerID, 
280      *                          or RoleDescriptor {@link #getRoleIDToken(String, RoleDescriptor)}
281      *                          corresponding to the element whose signature is being evaluated.
282      *                          This is used exclusively for logging/debugging purposes and
283      *                          should not be used operationally (e.g. for building a criteria set).
284      * @param isEntityGroup flag indicating whether the signed object is a metadata group (EntitiesDescriptor),
285      *                      primarily useful for constructing a criteria set for the trust engine
286      * @throws FilterException thrown if the metadata entry's signature can not be established as trusted,
287      *                         or if an error occurs during the signature verification process
288      */
289     protected void verifySignature(SignableXMLObject signedMetadata, String metadataEntryName, 
290             boolean isEntityGroup) throws FilterException {
291         
292         log.debug("Verifying signature on metadata entry: {}", metadataEntryName);
293         
294         Signature signature = signedMetadata.getSignature();
295         if (signature == null) {
296             // We shouldn't ever be calling this on things that aren't actually signed, but just to be safe...
297             log.warn("Signature was null, skipping processing on metadata entry: {}", metadataEntryName);
298             return;
299         }
300         
301         performPreValidation(signature, metadataEntryName);
302         
303         CriteriaSet criteriaSet = buildCriteriaSet(signedMetadata, metadataEntryName, isEntityGroup);
304         
305         try {
306             if ( getSignatureTrustEngine().validate(signature, criteriaSet) ) {
307                 log.trace("Signature trust establishment succeeded for metadata entry {}", metadataEntryName);
308                 return;
309             } else {
310                 log.error("Signature trust establishment failed for metadata entry {}", metadataEntryName);
311                 throw new FilterException("Signature trust establishment failed for metadata entry");
312             }
313         } catch (SecurityException e) {
314             // Treat evaluation errors as fatal
315             log.error("Error processing signature verification for metadata entry '{}': {} ",
316                     metadataEntryName, e.getMessage());
317             throw new FilterException("Error processing signature verification for metadata entry", e);
318         }
319     }
320 
321     /**
322      * Perform pre-validation on the Signature token.
323      * 
324      * @param signature the signature to evaluate
325      * @param metadataEntryName the EntityDescriptor entityID, EntitiesDescriptor Name,
326      *                          AffiliationDescriptor affiliationOwnerID, 
327      *                          or RoleDescriptor {@link #getRoleIDToken(String, RoleDescriptor)}
328      *                          corresponding to the element whose signature is being evaluated.
329      *                          This is used exclusively for logging/debugging purposes and
330      *                          should not be used operationally (e.g. for building a criteria set).
331      * @throws FilterException thrown if the signature element fails pre-validation
332      */
333     protected void performPreValidation(Signature signature, String metadataEntryName) throws FilterException {
334         if (getSignaturePrevalidator() != null) {
335             try {
336                 getSignaturePrevalidator().validate(signature);
337             } catch (ValidationException e) {
338                 log.error("Signature on metadata entry '{}' failed signature pre-validation", metadataEntryName);
339                 throw new FilterException("Metadata instance signature failed signature pre-validation", e);
340             }
341         }
342     }
343     
344     /**
345      * Build the criteria set which will be used as input to the configured trust engine.
346      * 
347      * @param signedMetadata the metadata element whose signature is being verified
348      * @param metadataEntryName the EntityDescriptor entityID, EntitiesDescriptor Name,
349      *                          AffiliationDescriptor affiliationOwnerID, 
350      *                          or RoleDescriptor {@link #getRoleIDToken(String, RoleDescriptor)}
351      *                          corresponding to the element whose signature is being evaluated.
352      *                          This is used exclusively for logging/debugging purposes and
353      *                          should not be used operationally (e.g. for building the criteria set).
354      * @param isEntityGroup flag indicating whether the signed object is a metadata group (EntitiesDescriptor)
355      * @return the newly constructed criteria set
356      */
357     protected CriteriaSet buildCriteriaSet(SignableXMLObject signedMetadata,
358             String metadataEntryName, boolean isEntityGroup) {
359         
360         CriteriaSet newCriteriaSet = new CriteriaSet();
361         
362         if (getDefaultCriteria() != null) {
363             newCriteriaSet.addAll( getDefaultCriteria() );
364         }
365         
366         if (!newCriteriaSet.contains(UsageCriteria.class)) {
367             newCriteriaSet.add( new UsageCriteria(UsageType.SIGNING) );
368         }
369         
370         // TODO how to handle adding dynamic entity ID and/or other criteria for trust engine consumption?
371         //
372         // Have 4 signed metadata types:
373         // 1) EntitiesDescriptor
374         // 2) EntityDescriptor
375         // 3) RoleDescriptor
376         // 4) AffiliationDescriptor
377         //
378         // Logic will likely vary for how to specify criteria to trust engine for different types + specific use cases,
379         // e.g. for federation metadata publishers of EntitiesDescriptors vs. "self-signed" EntityDescriptors.
380         // May need to delegate to more specialized subclasses.
381         
382         return newCriteriaSet;
383     }
384     
385     /**
386      * Get a string token for logging/debugging purposes that contains role information and containing entityID.
387      * 
388      * @param entityID the containing entityID
389      * @param role the role descriptor
390      * 
391      * @return the constructed role ID token.
392      */
393     protected String getRoleIDToken(String entityID, RoleDescriptor role) {
394         String roleName = role.getElementQName().getLocalPart();
395         return "[Role: " + entityID + "::" + roleName + "]";
396     }
397 }