View Javadoc

1   package org.astrogrid.security.wsse;
2   
3   import java.lang.reflect.Constructor;
4   import java.lang.reflect.InvocationTargetException;
5   import java.security.PrivateKey;
6   import java.security.cert.X509Certificate;
7   import javax.xml.namespace.QName;
8   import org.apache.commons.logging.Log;
9   import org.apache.commons.logging.LogFactory;
10  import org.apache.ws.security.WSSConfig;
11  import org.apache.ws.security.WSConstants;
12  import org.apache.ws.security.WSSecurityException;
13  import org.apache.ws.security.SOAPConstants;
14  import org.apache.ws.security.components.crypto.Crypto;
15  import org.apache.ws.security.message.EnvelopeIdResolver;
16  import org.apache.ws.security.message.token.BinarySecurity;
17  import org.apache.ws.security.message.token.PKIPathSecurity;
18  import org.apache.ws.security.message.token.Reference;
19  import org.apache.ws.security.message.token.SecurityTokenReference;
20  import org.apache.ws.security.message.token.X509Security;
21  import org.apache.ws.security.util.WSSecurityUtil;
22  import org.apache.xml.security.c14n.Canonicalizer;
23  import org.apache.xml.security.keys.KeyInfo;
24  import org.apache.xml.security.signature.XMLSignature;
25  import org.apache.xml.security.transforms.Transforms;
26  import org.apache.xml.security.utils.XMLUtils;
27  import org.apache.xml.security.utils.Constants;
28  import org.apache.xml.security.algorithms.SignatureAlgorithm;
29  import org.astrogrid.security.SecurityGuard;
30  import org.astrogrid.security.AxisServiceSecurityGuard;
31  import org.astrogrid.security.rfc3820.CertificateChainValidator;
32  import org.globus.gsi.TrustedCertificates;
33  import org.w3c.dom.Document;
34  import org.w3c.dom.Element;
35  import org.w3c.dom.Node;
36  import org.w3c.dom.NodeList;
37  
38  
39  import org.apache.ws.security.WSDocInfo;
40  import org.apache.ws.security.WSDocInfoStore;
41  
42  
43  /***
44   * A WS-Security header containing a digital signature on the body of a 
45   * SOAP message. Objects of this class are created by reading a DOM tree of
46   * a complete, signed SOAP message, as might be done in a web service that
47   * receives the message. The public methods allow the signature to be verified
48   * and an authenticated Principal to be extracted.
49   *
50   * @author Guy Rixon
51   */
52  public class WsseSignature {
53    
54    /***
55     * Creates a new instance of WsseSignature.
56     *
57     * @param document - the signed SOAP message.
58     * @param trustAnchors - the trusted certificates (may be null if the object is only used for signing)
59     */
60    public WsseSignature(Document document, TrustedCertificates trustAnchors) {
61      
62      // Remember the SOAP envelope.
63      this.message = document;
64      
65      // Remember the trust anchors.
66      this.trustAnchors = trustAnchors;
67      
68      // Make somewhere to put the authenticated identity and credentials.
69      // This will be empty until successful authentication.
70      this.authenticated = new AxisServiceSecurityGuard();
71      
72      // Use default settings for WSS4J
73      // @TODO get rid of this.
74      this.wssConfig = WSSConfig.getDefaultWSConfig();
75      
76      // Make a QName for the BinarySecurityTokn in the WSSE namespace of choice.
77      // @TODO get rid of this.
78      this.binaryToken = new QName(wssConfig.getWsseNS(), WSConstants.BINARY_TOKEN_LN);
79      
80      // Find and remember the security SOAP header. If there is no header, then
81      // this will return null.
82      // @TODO refactor away the SOAP constants.
83      SOAPConstants sc = WSSecurityUtil.getSOAPConstants(this.message.getDocumentElement());
84      this.header = WSSecurityUtil.getSecurityHeader(this.wssConfig, this.message, null, sc);
85    }
86    
87    /***
88     * Signs the SOAP message and returns the signed copy.
89     *
90     * @return - the signed message as a DOM document.
91     */
92    public Document sign(SecurityGuard guard) throws Exception {
93      
94      // Extract the certificate chain. This array contains all the
95      // certificates that will be sent with the signature but not
96      // the associated trust-anchors.
97      X509Certificate[] chain = guard.getCertificateChain();
98      if (chain.length == 0) {
99        throw new Exception("The given credentials do not contain a certificate chain.");
100     }
101     
102     // Extract the secret key from the given Subject.
103     PrivateKey privateKey = guard.getPrivateKey();
104     if (privateKey == null) {
105       throw new Exception("There is no private key in the given credentials.");
106     }
107     
108     // Create the signature structure.
109     XMLSignature sig = this.createSigatureElement(this.getSignatureAlgorithm(chain[0]));    
110     
111     // Create the binary security-token and associate it with the signature.
112     // The token carries the data from the certificate chain given in 
113     // the user credentials.
114     Reference ref = new Reference(wssConfig, this.message);
115     String certUri = "CertId-" + chain[0].hashCode();
116     ref.setURI("#" + certUri);
117     PKIPathSecurity bstToken
118         = new PKIPathSecurity(wssConfig, this.message);
119     bstToken.setX509Certificates(chain, false, new Uther());
120     bstToken.setID(certUri);
121     ref.setValueType(bstToken.getValueType());
122     SecurityTokenReference secRef = new SecurityTokenReference(wssConfig, this.message);
123     String strUri = "STRId-" + secRef.hashCode();
124     secRef.setID(strUri);
125     secRef.setReference(ref);
126     KeyInfo info = sig.getKeyInfo();
127     String keyInfoUri = "KeyId-" + info.hashCode();
128     info.setId(keyInfoUri);
129     info.addUnknownElement(secRef.getElement());
130 
131     // Sign the message. This call retains the cryptographic value of the
132     // signature inside the signature object.
133     sig.sign(privateKey);
134     
135     // Assemble the security header in the document.
136     Element securityHeader = insertSecurityHeader(this.message);
137     WSSecurityUtil.prependChildElement(this.message,
138                                        securityHeader,
139                                        sig.getElement(),
140                                        false);
141     WSSecurityUtil.prependChildElement(this.message,
142                                        securityHeader,
143                                        bstToken.getElement(),
144                                        false);
145 
146     log.debug("Signing complete.");
147     return (this.message);
148   }
149   
150   /***
151    * Verifies the signature and remembers the authenticated identity.
152    * An exception is thrown if the verification fails.
153    */
154   public void verify() throws Exception {
155     if (this.trustAnchors == null) {
156       throw new Exception("Signatures cannot be checked because " +
157                           "no trust-anchor certificates are loaded.");
158     }
159     
160     // If there's no security header - i.e. if this request message is
161     // anonymous - do nothing.
162     if (this.header == null) {
163       return;
164     }
165 
166     // Process the security SOAP-header: verify the signature cryptographically
167     // and return the certificate chain so that trust in the signing 
168     // certificate can later be verified. This code will throw an exception
169     // if the signature is invalid.
170     log.debug("Checking the signature...");
171     X509Certificate[] chain = this.processSecurityHeader(header);
172     log.debug("Signature is valid.");
173             
174     // Validate the chain of trust from the certificate that signed the message 
175     // to the trust anchor. This code throws an exception if trust 
176     // is not established.
177     // @TODO create the validator and TrustedCertificates at time of construction
178     // @TODO generalize the trust anchors.
179     // @TODO refactor the validator to hold the chain internally.
180     log.debug("Checking the certificate chain...");
181     CertificateChainValidator validator = new CertificateChainValidator();
182     validator.validateChain(chain, this.trustAnchors);
183     log.debug("Certificate chain is valid.");
184 
185     // Remember the authenticated identity and its certificate.
186     X509Certificate identityCertificate = validator.getIdentityCertificate();
187     this.authenticated.setX500Principal(identityCertificate.getSubjectX500Principal());
188     this.authenticated.setIdentityCertificate(identityCertificate);
189     this.authenticated.setCertificateChain(chain);
190   }
191   
192   /***
193    * Retrives the credentials and identities extracted from the message.
194    * The important datum is an X500Principal: this is the
195    * authenticated identity of the sender of the message. There may be
196    * additional data. The returned subject is never null, but it does not
197    * contain the X500Principal unless the signature has been successfully
198    * verified.
199    *
200    * @return - the JAAS subject holding the authenticated identity
201    */
202   public AxisServiceSecurityGuard getServiceGuard() {
203     return this.authenticated;
204   }
205   
206   /***
207    * Creates a digital-signature element in respect of the SOAP body.
208    */
209   protected XMLSignature createSigatureElement(SignatureAlgorithm signatureAlgorithm) throws Exception {
210     Element canonElem 
211         = XMLUtils.createElementInSignatureSpace(this.message,
212                                                  Constants._TAG_CANONICALIZATIONMETHOD);
213     canonElem.setAttributeNS(null, Constants._ATT_ALGORITHM, canonAlgo);
214     XMLSignature sig = new XMLSignature(this.message, null, signatureAlgorithm.getElement(), canonElem);
215     sig.addResourceResolver(EnvelopeIdResolver.getInstance(wssConfig));
216 
217     // Make a reference to the body of the SOAP message, which is the
218     // part to be signed.
219     SOAPConstants soapConstants = WSSecurityUtil.getSOAPConstants(this.message.getDocumentElement());
220     Element envelope = this.message.getDocumentElement();
221     Element body = (Element) WSSecurityUtil.findElement(envelope,
222                                                         soapConstants.getBodyQName().getLocalPart(),
223                                                         soapConstants.getEnvelopeURI());
224     if (body == null) {
225       throw new Exception("The message cannot be signed " +
226                           "because it has no SOAP body.");
227     }
228     Transforms transforms = new Transforms(this.message);
229     transforms.addTransform(Transforms.TRANSFORM_C14N_EXCL_OMIT_COMMENTS);
230     sig.addDocument("#" + setWsuId(body), transforms);
231     
232     return sig;
233   }
234   
235   /***
236    * Determines the algorithm used for the signature.
237    * The algorithm is chosen to match that used to sign the user's
238    * certificate. If that algorithm is not supported and exception is
239    * thrown.
240    */
241   protected SignatureAlgorithm getSignatureAlgorithm(X509Certificate certificate) throws Exception {
242     // Choose the signature algorithm to match the algorithm used in signing
243     // the user's certificate.
244     String pubKeyAlgo = certificate.getPublicKey().getAlgorithm();
245     String sigAlgo;
246     log.debug("automatic sig algo detection: " + pubKeyAlgo);
247     if (pubKeyAlgo.equalsIgnoreCase("DSA")) {
248       sigAlgo = XMLSignature.ALGO_ID_SIGNATURE_DSA;
249     }
250     else if (pubKeyAlgo.equalsIgnoreCase("RSA")) {
251       sigAlgo = XMLSignature.ALGO_ID_SIGNATURE_RSA;
252     }
253     else {
254       throw new Exception("The signature algorithm in the presented certificate  - " +
255                           pubKeyAlgo +
256                           " - is not supported.");
257     }
258     return new SignatureAlgorithm(this.message, sigAlgo);
259   }
260   
261   /***
262    * Process only the signature part of the header.
263    */
264   protected X509Certificate[] processSecurityHeader(Element securityHeader) throws Exception {
265     X509Certificate[] chain = null;
266 
267     // Find and process the first signature element. 
268     // Ignore following signature elements and all other elements.
269     NodeList list = securityHeader.getChildNodes();
270     int len = list.getLength();
271     for (int i = 0; i < len; i++) {
272       Node node = list.item(i);
273       if (node.getNodeType() == Node.ELEMENT_NODE) {
274         Element element = (Element)node;
275         QName qn = new QName(element.getNamespaceURI(), element.getLocalName());
276         if (qn.equals(SIGNATURE)) {
277           chain = this.processSignatureElement(element);
278         }
279       }
280     }
281       
282     return chain;
283   }
284             
285   protected X509Certificate[] processSignatureElement(Element element) throws Exception {
286     log.debug("Found signature element");
287       
288     // Creating this object validates some of the internal structure of the signature element.
289     // Invalid structures cause exceptions to be thrown here.
290     XMLSignature signature = new XMLSignature(element, null);
291     signature.addResourceResolver(new EnvelopeIdResolver(this.wssConfig, this.message));
292 
293     // The signature element must include a ds:keyInfo child that specifies
294     // where in the security header lies the public key for verifying the
295     // signature. If it's missing then this code can't verify the signature.
296     // (There are special modes where the receiver can infer the correct
297     // key from a pre-configured set, but they aren't supported here.)
298     KeyInfo info = signature.getKeyInfo();
299     if (info == null) {
300       throw new Exception("The signature in the message does not include " +
301                           "a reference to the key for checking the " +
302                           "signature; this usage is not supported.");
303     }
304     
305     // Parse the KeyInfo and recover the X.509 certificate-chain in the message.
306     X509Certificate[] chain = this.getCredentialsFromMessage(info, element);
307 
308     // Check the signature using the certificate at the
309     // head of the chain recovered from the security token.
310     // This does not validate the certificate itself.
311 	  if (!signature.checkSignatureValue(chain[0])) {
312       throw new Exception("The signature is cryptographically invalid.");
313     }
314     
315     return chain;
316   }
317   
318   
319   protected X509Certificate[] getCredentialsFromMessage(KeyInfo info, Element elem) throws Exception {
320     X509Certificate[] certs;
321     
322     Crypto crypto = new Uther();
323     
324             // Look up the security-token reference and get, from the token,
325         // the X.509 certificates.
326         Node node = WSSecurityUtil.getDirectChild(info.getElement(),
327 						SecurityTokenReference.SECURITY_TOKEN_REFERENCE,
328 						wssConfig.getWsseNS());
329 			if (node == null) {
330 				throw new WSSecurityException(
331 						WSSecurityException.INVALID_SECURITY,
332 						"unsupportedKeyInfo");
333 			}
334 			SecurityTokenReference secRef = new SecurityTokenReference(
335 					wssConfig, (Element) node);
336 
337 			int docHash = elem.getOwnerDocument().hashCode();
338 			/*
339 			 * Her we get some information about the document that is being
340 			 * processed, in partucular the crypto implementation, and already
341 			 * detected BST that may be used later during dereferencing.
342 			 */
343 			WSDocInfo wsDocInfo = WSDocInfoStore.lookup(docHash);
344 
345 			if (secRef.containsReference()) {
346 				Element token = secRef.getTokenElement(elem.getOwnerDocument(), wsDocInfo);
347 				if (token.getLocalName().equals(binaryToken.getLocalPart())) {
348 					certs = getCertificatesTokenReference((Element) token, crypto);
349 				} 
350         else {
351 					throw new Exception("The signature refers to a token of type " +
352                               "{" +
353                               token.getNamespaceURI() +
354                               "}" +
355                               token.getLocalName() +
356                               " which is not supported.");
357 				}	
358       } else if (secRef.containsX509IssuerSerial()) {
359 				certs = secRef.getX509IssuerSerial(crypto);
360 			} else if (secRef.containsKeyIdentifier()) {
361 				certs = secRef.getKeyIdentifier(crypto);
362 			} else {
363 				throw new WSSecurityException(
364 						WSSecurityException.INVALID_SECURITY,
365 						"unsupportedKeyInfo", new Object[] { node.toString() });
366 			}
367 
368     if (certs == null || certs.length == 0 || certs[0] == null) {
369       throw new Exception("No certificates were found in the message header.");
370     }
371     return certs;
372   }
373 
374   /***
375 	 * Extracts the certificate(s) from the Binary Security token reference.
376 	 * <p/>
377 	 *
378 	 * @param elem
379 	 *            The element containing the binary security token. This is
380 	 *            either X509 certificate(s) or a PKIPath.
381 	 * @return an array of X509 certificates
382 	 * @throws WSSecurityException
383 	 */
384     protected X509Certificate[] getCertificatesTokenReference(Element elem,
385                                                            Crypto crypto)
386             throws WSSecurityException {
387         BinarySecurity token = createSecurityToken(elem);
388         if (token instanceof PKIPathSecurity) {
389             return ((PKIPathSecurity) token).getX509Certificates(false, crypto);
390         } else if (token instanceof X509Security) {
391             X509Certificate cert = ((X509Security) token).getX509Certificate(crypto);
392             X509Certificate[] certs = new X509Certificate[1];
393             certs[0] = cert;
394             return certs;
395         } else {
396             throw new WSSecurityException(WSSecurityException.UNSUPPORTED_SECURITY_TOKEN,
397                     "unhandledToken", new Object[]{token.getClass().getName()});
398         }
399     }
400 
401     /***
402      * Checks the <code>element</code> and creates appropriate binary security object.
403      *
404      * @param element The XML element that contains either a <code>BinarySecurityToken
405      *                </code> or a <code>PKIPath</code> element. Other element types a not
406      *                supported
407      * @return the BinarySecurity object, either a <code>X509Security</code> or a
408      *         <code>PKIPathSecurity</code> object.
409      * @throws WSSecurityException
410      */
411     private BinarySecurity createSecurityToken(Element element) throws WSSecurityException {
412         BinarySecurity token = new BinarySecurity(wssConfig, element);
413         String type = token.getValueType();
414         Class clazz = null;
415         if (wssConfig.getProcessNonCompliantMessages() ||
416                 wssConfig.isBSTValuesPrefixed()) {
417             if (type.endsWith(X509Security.X509_V3)) {
418                 clazz = X509Security.class;
419             } else if (type.endsWith(PKIPathSecurity.X509PKI_PATH)) {
420                 clazz = PKIPathSecurity.class;
421             }
422         } else {
423             if (type.equals(X509Security.getType(wssConfig))) {
424                 clazz = X509Security.class;
425             } else if (type.equals(PKIPathSecurity.getType(wssConfig))) {
426                 clazz = PKIPathSecurity.class;
427             }
428         }
429         if (clazz == null) {
430             throw new WSSecurityException(WSSecurityException.UNSUPPORTED_SECURITY_TOKEN,
431                     "unsupportedBinaryTokenType", new Object[]{type});
432         }
433         try {
434             Constructor constructor = clazz.getConstructor(constructorType);
435             if (constructor == null) {
436                 throw new WSSecurityException(WSSecurityException.FAILURE,
437                         "invalidConstructor", new Object[]{clazz});
438             }
439             return (BinarySecurity) constructor.newInstance(new Object[]{wssConfig, element});
440         } catch (InvocationTargetException e) {
441             Throwable ee = e.getTargetException();
442             if (ee instanceof WSSecurityException) {
443                 throw (WSSecurityException) ee;
444             } else {
445                 throw new WSSecurityException(WSSecurityException.FAILURE, null, null, e);
446             }
447         } catch (NoSuchMethodException e) {
448             throw new WSSecurityException(WSSecurityException.FAILURE, null, null, e);
449         } catch (InstantiationException e) {
450             throw new WSSecurityException(WSSecurityException.FAILURE, null, null, e);
451         } catch (IllegalAccessException e) {
452             throw new WSSecurityException(WSSecurityException.FAILURE, null, null, e);
453         }
454     }
455     
456    /***
457      * Creates a security header and inserts it as child into the SOAP Envelope.
458      * <p/>
459      * Check if a WS Security header block for an actor is already available
460      * in the document. If a header block is found return it, otherwise a new
461      * wsse:Security header block is created and the attributes set
462      *
463      * @param doc A SOAP envelope as <code>Document</code>
464      * @return A <code>wsse:Security</code> element
465      */
466     protected Element insertSecurityHeader(Document doc) {
467         SOAPConstants soapConstants =
468                 WSSecurityUtil.getSOAPConstants(doc.getDocumentElement());
469         // lookup a security header block that matches actor
470         Element securityHeader =
471                 WSSecurityUtil.getSecurityHeader(wssConfig, doc, null, soapConstants);
472         if (securityHeader == null) { // create if nothing found
473             securityHeader =
474                     WSSecurityUtil.findWsseSecurityHeaderBlock(wssConfig,
475                             doc,
476                             doc.getDocumentElement(),
477                             null,
478                             true);
479         }
480         return securityHeader;
481     }
482     
483     protected String setWsuId(Element bodyElement) {
484         String id = null;
485         // try to get a differently qualified Id in case it was created with
486         // an older spec namespace
487         if (wssConfig.getProcessNonCompliantMessages()) {
488             id = WSSecurityUtil.getAttributeValueWSU(bodyElement, "Id", null);
489         }
490         if (wssConfig.getProcessNonCompliantMessages() ||
491                 !wssConfig.isTargetIdQualified()) {
492             if ((id == null) || (id.length() == 0)) {
493                 id = bodyElement.getAttribute("Id");
494             }
495         } else {
496             id = bodyElement.getAttributeNS(wssConfig.getWsuNS(), "Id");
497         }
498         if ((id == null) || (id.length() == 0)) {
499             id = "id-" + Integer.toString(bodyElement.hashCode());
500             if (wssConfig.isTargetIdQualified()) {
501                 String prefix =
502                         WSSecurityUtil.setNamespace(bodyElement,
503                                 wssConfig.getWsuNS(),
504                                 WSConstants.WSU_PREFIX);
505                 bodyElement.setAttributeNS(wssConfig.getWsuNS(), prefix + ":Id", id);
506             } else {
507                 bodyElement.setAttributeNS(null, "Id", id);
508             }
509         }
510         return id;
511     }
512   
513   /***
514    * The DOM tree holding the signed message.
515    */
516   private Document message;
517   
518   /***
519    * The set of trusted certificates used to check signatures.
520    */
521   private TrustedCertificates trustAnchors;
522   
523   /***
524    * The WS-Security SOAP header as a DOM fragment.
525    */
526   private Element header;
527   
528   /***
529    * The credentials extracted from the message.
530    */
531   private AxisServiceSecurityGuard authenticated;
532   
533   
534   
535       /***
536      * <code>wsse:BinarySecurityToken</code> as defined by WS Security specification.
537      */
538     protected QName binaryToken;
539     
540     /***
541      * <code>ds:Signature</code> as defined by XML-signature specification,
542      * enhanced by WS Security specification.
543      */
544     protected static final QName SIGNATURE = new QName(WSConstants.SIG_NS, WSConstants.SIG_LN);
545     
546     protected String canonAlgo = Canonicalizer.ALGO_ID_C14N_EXCL_OMIT_COMMENTS;
547     
548     protected WSSConfig wssConfig;
549     
550     private static final Class[] constructorType = {WSSConfig.class, org.w3c.dom.Element.class};
551     
552     private static Log log = LogFactory.getLog(WsseSignature.class.getName());
553 }