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.xml.security.keyinfo;
18  
19  import java.security.Key;
20  import java.security.KeyException;
21  import java.security.PrivateKey;
22  import java.security.PublicKey;
23  import java.util.ArrayList;
24  import java.util.Collection;
25  import java.util.HashSet;
26  import java.util.List;
27  import java.util.Set;
28  
29  import javax.crypto.SecretKey;
30  
31  import org.opensaml.xml.XMLObject;
32  import org.opensaml.xml.security.CriteriaSet;
33  import org.opensaml.xml.security.SecurityException;
34  import org.opensaml.xml.security.SecurityHelper;
35  import org.opensaml.xml.security.credential.AbstractCriteriaFilteringCredentialResolver;
36  import org.opensaml.xml.security.credential.BasicCredential;
37  import org.opensaml.xml.security.credential.Credential;
38  import org.opensaml.xml.signature.KeyInfo;
39  import org.opensaml.xml.signature.KeyName;
40  import org.opensaml.xml.signature.KeyValue;
41  import org.slf4j.Logger;
42  import org.slf4j.LoggerFactory;
43  
44  /**
45   * Implementation of {@link KeyInfoCredentialResolver} which resolves credentials based on a {@link KeyInfo} element
46   * using a configured list of {@link KeyInfoProvider}'s and optional post-processing hooks.
47   * 
48   * <p>
49   * The majority of the processing of the KeyInfo and extraction of {@link Credential}'s from the KeyInfo is handled by
50   * instances of {@link KeyInfoProvider}. An ordered list of KeyInfoProviders must be supplied to the resolver when it
51   * is constructed.
52   * </p>
53   * 
54   * <p>
55   * This resolver requires a {@link KeyInfoCriteria} to be supplied as the resolution criteria. It is permissible,
56   * however, for the criteria's KeyInfo data object to be null. This allows for more convenient processing logic, for
57   * example, in cases when a parent element allows an optional KeyInfo and when in fact a given instance does not contain
58   * one. Specialized subclasses of this resolver may still attempt to return credentials in an implementation or
59   * context-specific manner, as described below.
60   * </p>
61   * 
62   * <p>
63   * Processing of the supplied KeyInfo element proceeds as follows:
64   * <ol>
65   * <li>A {@link KeyInfoResolutionContext} is instantiated. This resolution context is used to hold state shared amongst
66   * all the providers and processing hooks which run within the resolver.</li>
67   * <li>This resolution context is initialized and populated with the actual KeyInfo object being processed as well as
68   * the values of any {@link KeyName} child elements present.</li>
69   * <li>An attempt is then made to resolve a credential from any {@link KeyValue} child elements as described for
70   * {@link #resolveKeyValue(KeyInfoResolutionContext, CriteriaSet, List)} If a credential is so resolved, its key will
71   * also be placed in the resolution context</li>
72   * <li>The remaining (non-KeyValue) children are then processed in document order. Each child element is processed by
73   * the registered providers in provider list order. The credential or credentials resolved by the first provider to
74   * successfully do so are added to the effective set of credentials returned by the resolver, and processing of that
75   * child element terminates. Processing continues with the next child element.</li>
76   * <li>At this point all KeyInfo children have been processed. If the effective set of credentials to return is empty,
77   * and if a key was resolved from a KeyValue element and is available in the resolution context, a basic credential is
78   * built with that key and is added to the effective set. Since the KeyInfo may have a plain KeyValue representation of
79   * the key represented by the KeyInfo, in addition to a more specific key type/container (and hence credential)
80   * representation, this technique avoids the unnecessary return of duplicate keys, returning only the more specific
81   * credential representation of the key.</li>
82   * <li>A post-processing hook is then called: {@link #postProcess(KeyInfoResolutionContext, CriteriaSet, List)}. The
83   * default implementation is a no-op. This is an extension point by which subclasses may implement custom
84   * post-processing of the effective credential set to be returned. One example use case is when the KeyInfo being
85   * processed represents the public aspects (e.g. public key, or a key name or other identifier) of an encryption key
86   * belonging to the resolving entity. The resolved public keys and other resolution context information may be used to
87   * further resolve the credential or credentials containing the associated decryption key (i.e. a private or symmetric
88   * key). For an example of such an implementation, see {@link LocalKeyInfoCredentialResolver}</li>
89   * <li>Finally, if no credentials have been otherwise resolved, a final post-processing hook is called:
90   * {@link #postProcessEmptyCredentials(KeyInfoResolutionContext, CriteriaSet, List)}. The default implementation is a
91   * no-op. This is an extension point by which subclasses may implement custom logic to resolve credentials in an
92   * implementation or context-specific manner, if no other mechanism has succeeded. Example usages might be to return a
93   * default set of credentials, or to use non-KeyInfo-derived criteria or contextual information to determine the
94   * credential or credentials to return.</li>
95   * </ol>
96   * </p>
97   * 
98   */
99  public class BasicProviderKeyInfoCredentialResolver extends AbstractCriteriaFilteringCredentialResolver implements
100         KeyInfoCredentialResolver {
101 
102     /** Class logger. */
103     private final Logger log = LoggerFactory.getLogger(BasicProviderKeyInfoCredentialResolver.class);
104 
105     /** List of KeyInfo providers that are registered on this instance. */
106     private List<KeyInfoProvider> providers;
107 
108     /**
109      * Constructor.
110      * 
111      * @param keyInfoProviders the list of KeyInfoProvider's to use in this resolver
112      */
113     public BasicProviderKeyInfoCredentialResolver(List<KeyInfoProvider> keyInfoProviders) {
114         super();
115 
116         providers = new ArrayList<KeyInfoProvider>();
117         providers.addAll(keyInfoProviders);
118     }
119 
120     /**
121      * Return the list of the KeyInfoProvider instances used in this resolver configuration.
122      * 
123      * @return the list of providers configured for this resolver instance
124      */
125     protected List<KeyInfoProvider> getProviders() {
126         return providers;
127     }
128 
129     /** {@inheritDoc} */
130     protected Iterable<Credential> resolveFromSource(CriteriaSet criteriaSet) throws SecurityException {
131         KeyInfoCriteria kiCriteria = criteriaSet.get(KeyInfoCriteria.class);
132         if (kiCriteria == null) {
133             log.error("No KeyInfo criteria supplied, resolver could not process");
134             throw new SecurityException("Credential criteria set did not contain an instance of"
135                     + "KeyInfoCredentialCriteria");
136         }
137         KeyInfo keyInfo = kiCriteria.getKeyInfo();
138 
139         // This will be the list of credentials to return.
140         List<Credential> credentials = new ArrayList<Credential>();
141 
142         KeyInfoResolutionContext kiContext = new KeyInfoResolutionContext(credentials);
143 
144         // Note: we allow KeyInfo to be null to handle case where application context,
145         // other accompanying criteria, etc, should be used to resolve credentials via hooks below.
146         if (keyInfo != null) {
147             processKeyInfo(keyInfo, kiContext, criteriaSet, credentials);
148         } else {
149             log.info("KeyInfo was null, any credentials will be resolved by post-processing hooks only");
150         }
151 
152         // Postprocessing hook
153         postProcess(kiContext, criteriaSet, credentials);
154 
155         // Final empty credential hook
156         if (credentials.isEmpty()) {
157             log.debug("No credentials were found, calling empty credentials post-processing hook");
158             postProcessEmptyCredentials(kiContext, criteriaSet, credentials);
159         }
160 
161         log.debug("A total of {} credentials were resolved", credentials.size());
162         return credentials;
163     }
164 
165     /**
166      * The main processing logic implemented by this resolver.
167      * 
168      * @param keyInfo the KeyInfo being processed
169      * @param kiContext KeyInfo resolution context
170      * @param criteriaSet the credential criteria used to resolve credentials
171      * @param credentials the list which will store the resolved credentials
172      * @throws SecurityException thrown if there is an error during processing
173      */
174     private void processKeyInfo(KeyInfo keyInfo, KeyInfoResolutionContext kiContext, CriteriaSet criteriaSet,
175             List<Credential> credentials) throws SecurityException {
176 
177         // Initialize the resolution context that will be used by the provider plugins.
178         // This processes the KeyName and the KeyValue children, if either are present.
179         initResolutionContext(kiContext, keyInfo, criteriaSet);
180 
181         // Store these off so we later use the original values,
182         // unmodified by other providers which later run.
183         Key keyValueKey = kiContext.getKey();
184         HashSet<String> keyNames = new HashSet<String>();
185         keyNames.addAll(kiContext.getKeyNames());
186 
187         // Now process all (non-KeyValue) children
188         processKeyInfoChildren(kiContext, criteriaSet, credentials);
189 
190         if (credentials.isEmpty() && keyValueKey != null) {
191             // Add the credential based on plain KeyValue if no more specifc cred type was found
192             Credential keyValueCredential = buildBasicCredential(keyValueKey, keyNames);
193             if (keyValueCredential != null) {
194                 log.debug("No credentials were extracted by registered non-KeyValue handling providers, "
195                         + "adding KeyValue credential to returned credential set");
196                 credentials.add(keyValueCredential);
197             }
198         }
199     }
200 
201     /**
202      * Hook for subclasses to do post-processing of the credential set after all KeyInfo children have been processed.
203      * 
204      * For example, the previously resolved credentials might be used to index into a store of local credentials, where
205      * the index is a key name or the public half of a key pair extracted from the KeyInfo.
206      * 
207      * @param kiContext KeyInfo resolution context
208      * @param criteriaSet the credential criteria used to resolve credentials
209      * @param credentials the list which will store the resolved credentials
210      * @throws SecurityException thrown if there is an error during processing
211      */
212     protected void postProcess(KeyInfoResolutionContext kiContext, CriteriaSet criteriaSet, List<Credential> credentials)
213             throws SecurityException {
214 
215     }
216 
217     /**
218      * Hook for processing the case where no credentials were returned by any resolution method by any provider, nor by
219      * the processing of the {@link #postProcess(KeyInfoResolutionContext, CriteriaSet, List)} hook.
220      * 
221      * @param kiContext KeyInfo resolution context
222      * @param criteriaSet the credential criteria used to resolve credentials
223      * @param credentials the list which will store the resolved credentials
224      * 
225      * @throws SecurityException thrown if there is an error during processing
226      */
227     protected void postProcessEmptyCredentials(KeyInfoResolutionContext kiContext, CriteriaSet criteriaSet,
228             List<Credential> credentials) throws SecurityException {
229 
230     }
231 
232     /**
233      * Use registered providers to process the non-KeyValue children of KeyInfo.
234      * 
235      * Each child element is processed in document order. Each child element is processed by each provider in the
236      * ordered list of providers. The credential or credentials resolved by the first provider to successfully do so are
237      * added to the effective set resolved by the KeyInfo resolver.
238      * 
239      * @param kiContext KeyInfo resolution context
240      * @param criteriaSet the credential criteria used to resolve credentials
241      * @param credentials the list which will store the resolved credentials
242      * @throws SecurityException thrown if there is a provider error processing the KeyInfo children
243      */
244     protected void processKeyInfoChildren(KeyInfoResolutionContext kiContext, CriteriaSet criteriaSet,
245             List<Credential> credentials) throws SecurityException {
246 
247         for (XMLObject keyInfoChild : kiContext.getKeyInfo().getXMLObjects()) {
248 
249             if (keyInfoChild instanceof KeyValue) {
250                 continue;
251             }
252 
253             log.debug("Processing KeyInfo child with qname: {}", keyInfoChild.getElementQName());
254             Collection<Credential> childCreds = processKeyInfoChild(kiContext, criteriaSet, keyInfoChild);
255 
256             if (childCreds != null && !childCreds.isEmpty()) {
257                 credentials.addAll(childCreds);
258             } else {
259                 // Not really an error or warning if KeyName doesn't produce a credential
260                 if (keyInfoChild instanceof KeyName) {
261                     log.debug("KeyName, with value {}, did not independently produce a credential based on any registered providers",
262                                     ((KeyName) keyInfoChild).getValue());
263 
264                 } else {
265                     log.warn("No credentials could be extracted from KeyInfo child with qname {} by any registered provider",
266                                     keyInfoChild.getElementQName());
267                 }
268             }
269         }
270     }
271 
272     /**
273      * Process the given KeyInfo child with the registered providers.
274      * 
275      * The child element is processed by each provider in the ordered list of providers. The credential or credentials
276      * resolved by the first provider to successfully do so are returned and processing of the child element is
277      * terminated.
278      * 
279      * @param kiContext KeyInfo resolution context
280      * @param criteriaSet the credential criteria used to resolve credentials
281      * @param keyInfoChild the KeyInfo to evaluate
282      * @return the collection of resolved credentials, or null
283      * @throws SecurityException thrown if there is a provider error processing the KeyInfo child
284      */
285     protected Collection<Credential> processKeyInfoChild(KeyInfoResolutionContext kiContext, CriteriaSet criteriaSet,
286             XMLObject keyInfoChild) throws SecurityException {
287 
288         for (KeyInfoProvider provider : getProviders()) {
289 
290             if (!provider.handles(keyInfoChild)) {
291                 log.debug("Provider {} doesn't handle objects of type {}, skipping", provider.getClass().getName(),
292                         keyInfoChild.getElementQName());
293                 continue;
294             }
295 
296             log.debug("Processing KeyInfo child {} with provider {}", keyInfoChild.getElementQName(), provider
297                     .getClass().getName());
298             Collection<Credential> creds = provider.process(this, keyInfoChild, criteriaSet, kiContext);
299 
300             if (creds != null && !creds.isEmpty()) {
301                 log.debug("Credentials successfully extracted from child {} by provider {}", keyInfoChild
302                         .getElementQName(), provider.getClass().getName());
303                 return creds;
304             }
305         }
306         return null;
307     }
308 
309     /**
310      * Initialize the resolution context that will be used by the providers.
311      * 
312      * The supplied KeyInfo object is stored in the context, as well as the values of any {@link KeyName} children
313      * present. Finally if a credential is resolveble by any registered provider from a plain {@link KeyValue} child,
314      * the key from that credential is also stored in the context.
315      * 
316      * @param kiContext KeyInfo resolution context
317      * @param keyInfo the KeyInfo to evaluate
318      * @param criteriaSet the credential criteria used to resolve credentials
319      * @throws SecurityException thrown if there is an error processing the KeyValue children
320      */
321     protected void initResolutionContext(KeyInfoResolutionContext kiContext, KeyInfo keyInfo, CriteriaSet criteriaSet)
322             throws SecurityException {
323 
324         kiContext.setKeyInfo(keyInfo);
325 
326         // Extract all KeyNames
327         kiContext.getKeyNames().addAll(KeyInfoHelper.getKeyNames(keyInfo));
328         log.debug("Found {} key names: {}", kiContext.getKeyNames().size(), kiContext.getKeyNames());
329 
330         // Extract the Credential based on the (singular) key from an existing KeyValue(s).
331         resolveKeyValue(kiContext, criteriaSet, keyInfo.getKeyValues());
332     }
333 
334     /**
335      * Resolve the key from any KeyValue element that may be present, and store the resulting key in the resolution
336      * context.
337      * 
338      * Each KeyValue element is processed in turn in document order. Each Keyvalue will be processed by each provider in
339      * the ordered list of registered providers. The key from the first credential successfully resolved from a KeyValue
340      * will be stored in the resolution context.
341      * 
342      * Note: This resolver implementation assumes that KeyInfo/KeyValue will not be abused via-a-vis the Signature
343      * specificiation, and that therefore all KeyValue elements (if there is even more than one) will all resolve to the
344      * same key value. The KeyInfo might, for example have multiple KeyValue children, containing different
345      * representations of the same key. Therefore, only the first credential derived from a KeyValue will be be
346      * utilized.
347      * 
348      * @param kiContext KeyInfo resolution context
349      * @param criteriaSet the credential criteria used to resolve credentials
350      * @param keyValues the KeyValue children to evaluate
351      * @throws SecurityException thrown if there is an error resolving the key from the KeyValue
352      */
353     protected void resolveKeyValue(KeyInfoResolutionContext kiContext, CriteriaSet criteriaSet, List<KeyValue> keyValues)
354             throws SecurityException {
355 
356         for (KeyValue keyValue : keyValues) {
357             Collection<Credential> creds = processKeyInfoChild(kiContext, criteriaSet, keyValue);
358             if (creds != null) {
359                 for (Credential cred : creds) {
360                     Key key = extractKeyValue(cred);
361                     if (key != null) {
362                         kiContext.setKey(key);
363                         log.debug("Found a credential based on a KeyValue having key type: {}", key.getAlgorithm());
364                         return;
365                     }
366                 }
367             }
368         }
369     }
370 
371     /**
372      * Construct a basic credential containing the specified key and set of key names.
373      * 
374      * @param key the key to include in the credential
375      * @param keyNames the key names to include in the credential
376      * @return a basic credential with the specified key and key names
377      * @throws SecurityException if there is an error building the credential
378      */
379     protected Credential buildBasicCredential(Key key, Set<String> keyNames) throws SecurityException {
380         if (key == null) {
381             log.debug("Key supplied was null, could not build credential");
382             return null;
383         }
384 
385         BasicCredential basicCred = new BasicCredential();
386 
387         basicCred.getKeyNames().addAll(keyNames);
388 
389         if (key instanceof PublicKey) {
390             basicCred.setPublicKey((PublicKey) key);
391         } else if (key instanceof SecretKey) {
392             basicCred.setSecretKey((SecretKey) key);
393         } else if (key instanceof PrivateKey) {
394             // This would be unusual for most KeyInfo use cases,
395             // but go ahead and try and handle it
396             PrivateKey privateKey = (PrivateKey) key;
397             try {
398                 PublicKey publicKey = SecurityHelper.derivePublicKey(privateKey);
399                 if (publicKey != null) {
400                     basicCred.setPublicKey(publicKey);
401                     basicCred.setPrivateKey(privateKey);
402                 } else {
403                     log.error("Failed to derive public key from private key");
404                     return null;
405                 }
406             } catch (KeyException e) {
407                 log.error("Could not derive public key from private key", e);
408                 return null;
409             }
410         } else {
411             log.error(String.format("Key was of an unsupported type '%s'", key.getClass().getName()));
412             return null;
413         }
414 
415         return basicCred;
416     }
417 
418     /**
419      * Utility method to extract any key that might be present in the specified Credential.
420      * 
421      * @param cred the Credential to evaluate
422      * @return the Key contained in the credential, or null if it does not contain a key.
423      */
424     protected Key extractKeyValue(Credential cred) {
425         if (cred == null) {
426             return null;
427         }
428         if (cred.getPublicKey() != null) {
429             return cred.getPublicKey();
430         }
431         // This could happen if key is derived, e.g. key agreement, etc
432         if (cred.getSecretKey() != null) {
433             return cred.getSecretKey();
434         }
435         // Perhaps unlikely, but go ahead and check
436         if (cred.getPrivateKey() != null) {
437             return cred.getPrivateKey();
438         }
439         return null;
440     }
441 
442 }