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
63 this.message = document;
64
65
66 this.trustAnchors = trustAnchors;
67
68
69
70 this.authenticated = new AxisServiceSecurityGuard();
71
72
73
74 this.wssConfig = WSSConfig.getDefaultWSConfig();
75
76
77
78 this.binaryToken = new QName(wssConfig.getWsseNS(), WSConstants.BINARY_TOKEN_LN);
79
80
81
82
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
95
96
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
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
109 XMLSignature sig = this.createSigatureElement(this.getSignatureAlgorithm(chain[0]));
110
111
112
113
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
132
133 sig.sign(privateKey);
134
135
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
161
162 if (this.header == null) {
163 return;
164 }
165
166
167
168
169
170 log.debug("Checking the signature...");
171 X509Certificate[] chain = this.processSecurityHeader(header);
172 log.debug("Signature is valid.");
173
174
175
176
177
178
179
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
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
218
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
243
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
268
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
289
290 XMLSignature signature = new XMLSignature(element, null);
291 signature.addResourceResolver(new EnvelopeIdResolver(this.wssConfig, this.message));
292
293
294
295
296
297
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
306 X509Certificate[] chain = this.getCredentialsFromMessage(info, element);
307
308
309
310
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
325
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
340
341
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
470 Element securityHeader =
471 WSSecurityUtil.getSecurityHeader(wssConfig, doc, null, soapConstants);
472 if (securityHeader == null) {
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
486
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 }